Производительность Expression.Compile vs Lambda, прямые виртуальные вызовы

Мне любопытно, как исполнитель Expression.Compile отличается от выражения лямбда в коде и против прямого использования метода, а также прямыми вызовами метода vs вызовы виртуальных методов (псевдокод):

var foo = new Foo();
var iFoo = (IFoo)foo;

foo.Bar();
iFoo.Bar();
(() => foo.Bar())();
(() => iFoo.Bar())();
Expression.Compile(foo, Foo.Bar)();
Expression.Compile(iFoo, IFoo.Bar)();
Expression.CompileToMethod(foo, Foo.Bar);
Expression.CompileToMethod(iFoo, IFoo.Bar);
MethodInfo.Invoke(foo, Foo.Bar);
MethodInfo.Invoke(iFoo, IFoo.Bar);

Ответ 1

Я не нашел ответа, поэтому вот тест производительности:

using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

namespace ExpressionTest
{
    public interface IFoo
    {
        int Bar();
    }

    public sealed class FooImpl : IFoo
    {
        public int Bar()
        {
            return 0;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var foo = new FooImpl();
            var iFoo = (IFoo)foo;

            Func<int> directLambda = () => foo.Bar();
            Func<int> virtualLambda = () => iFoo.Bar();
            var compiledDirectCall = CompileBar(foo, asInterfaceCall: false);
            var compiledVirtualCall = CompileBar(foo, asInterfaceCall: true);
            var compiledArgDirectCall = CompileBar<FooImpl>();
            var compiledArgVirtualCall = CompileBar<IFoo>();
            var barMethodInfo = typeof(FooImpl).GetMethod(nameof(FooImpl.Bar));
            var iBarMethodInfo = typeof(IFoo).GetMethod(nameof(IFoo.Bar));
            var compiledToModuleDirect = CompileToModule<FooImpl>();
            var compiledToModuleVirtual = CompileToModule<IFoo>();

            var iterationCount = 200000000;
            Console.WriteLine($"Iteration count: {iterationCount:N0}");

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < iterationCount; i++)
                compiledVirtualCall();
            var elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledToModuleVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.CompileToMethod(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledToModuleDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.CompileToMethod(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                virtualLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual () => IFoo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                directLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct () => FooImpl.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                iFoo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual IFoo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                foo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct Foo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++) {
                int result = (int)iBarMethodInfo.Invoke(iFoo, null);
            }
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual MethodInfo.Invoke(FooImpl, Bar): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++) {
                int result = (int)barMethodInfo.Invoke(foo, null);
            }
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct MethodInfo.Invoke(IFoo, Bar): {elapsedMs} ms");
        }

        static Func<int> CompileBar(IFoo foo, bool asInterfaceCall)
        {
            var fooType = asInterfaceCall ? typeof(IFoo) : foo.GetType();
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Constant(foo, fooType);
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call);
            var compiledFunction = (Func<int>)lambda.Compile();
            return compiledFunction;
        }

        static Func<TInput, int> CompileBar<TInput>()
        {
            var fooType = typeof(TInput);
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Parameter(fooType, "foo");
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call, instance);
            var compiledFunction = (Func<TInput, int>)lambda.Compile();
            return compiledFunction;
        }

        static Func<TInput, int> CompileToModule<TInput>()
        {
            var fooType = typeof(TInput);
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Parameter(fooType, "foo");
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call, instance);

            var asmName = new AssemblyName(fooType.Name);
            var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
            var moduleBuilder = asmBuilder.DefineDynamicModule(fooType.Name);
            var typeBuilder = moduleBuilder.DefineType(fooType.Name, TypeAttributes.Public);
            var methodBuilder = typeBuilder.DefineMethod(nameof(IFoo.Bar), MethodAttributes.Static, typeof(int), new[] { fooType });
            Expression.Lambda<Action>(lambda).CompileToMethod(methodBuilder);
            var createdType = typeBuilder.CreateType();

            var mi = createdType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)[1];
            var func = Delegate.CreateDelegate(typeof(Func<TInput, int>), mi);
            return (Func<TInput, int>)func;
        }
    }
}

На моем ноутбуке (режим Release, 64 бит,.NET 4.5.2) он дает:

Iteration count: 200,000,000
Virtual MethodInfo.Invoke(FooImpl, Bar):               61811 ms
Direct MethodInfo.Invoke(IFoo, Bar):                   37078 ms
Virtual (Func<int>)Expression.Compile():                2894 ms
Direct (Func<int>)Expression.Compile():                 2242 ms
Virtual (Func<IFoo, int>)Expression.Compile():          2319 ms
Direct (Func<FooImpl, int>)Expression.Compile():        2051 ms
Virtual (Func<IFoo, int>)Expression.CompileToMethod():   996 ms
Direct (Func<FooImpl, int>)Expression.CompileToMethod(): 679 ms
Virtual () => IFoo.Bar():                                796 ms
Direct () => FooImpl.Bar():                              469 ms
Virtual IFoo.Bar():                                      531 ms
Direct Foo.Bar():                                         68 ms

Надеюсь, что это поможет.

Ответ 2

Мы можем разделить вопрос на 2 случая:

  • как работает .NET с вызовом метода (вопрос инфраструктуры)?
  • Как оптимизаторы помогают вызовам методов?

ExpressionTest.exe в Release режиме с оптимизацией (настройки выпуска по умолчанию).NET 4.5.2:

Compiled Virtual Call: 4625 ms
Compiled Direct Call: 3361 ms
Lambda Virtual Call: 1096 ms
Lambda Direct Call: 576 ms
Virtual Call: 649 ms
Direct Call: 144 ms

Мы видим, что "Прямой вызов" в 4,5 раза быстрее, чем "Виртуальный вызов". Но, как мы видим выше, никакого вызова вообще нет. Метод Bar был встроен.

ExpressionTest.exe в Release режиме без оптимизации.NET 4.5.2:

Compiled Virtual Call: 5394 ms
Compiled Direct Call: 4666 ms
Lambda Virtual Call: 1800 ms
Lambda Direct Call: 1683 ms
Virtual Call: 1154 ms
Direct Call: 1112 ms

Итак, "Прямой вызов" примерно на 3-4% быстрее, чем "Виртуальный вызов".

Аналогичный вопрос: Производительность "direct" виртуальный вызов против вызова интерфейса в С#

Ответ 3

Подсказка: в режиме деблокирования вообще не происходит никакого вызова в случае "прямого вызова". CPU, идущий от 00B531BC (mov eax...) до 00B531C8 (jl 00B531BC).

                for (int i = 0; i < iterationCount; i++)
00B531BA  xor         edx,edx  
                foo.Bar();
00B531BC  mov         eax,dword ptr [ebx+4]  // actual loop begin
00B531BF  cmp         byte ptr [eax],al  
            for (int i = 0; i < iterationCount; i++)
00B531C1  inc         edx  
00B531C2  cmp         edx,0BEBC200h // 0BEBC200h = 200000000
00B531C8  jl          00B531BC      // loop begin address