Почему скомпилированная лямбда-сборка поверх Expression.Call немного медленнее, чем делегат, который должен делать то же самое?

Почему скомпилированная лямбда-сборка поверх Expression.Call немного медленнее, чем делегат, который должен делать то же самое? И как этого избежать?

Объяснение результатов BenchmarkDotNet. Мы сравниваем CallBuildedReal vs CallLambda; два других CallBuilded и CallLambdaConst являются «подчиненными формами» CallLambda и показывают равные числа. Но разница с CallBuildedReal существенная.

//[Config(typeof(Config))]
[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
[ClrJob , CoreJob]
[HtmlExporter, MarkdownExporter]
[MemoryDiagnoser /*, InliningDiagnoser*/]
public class BenchmarkCallSimple
{
    static Func<StringBuilder, int, int, bool> callLambda;
    static Func<StringBuilder, int, int, bool> callLambdaConst;
    static Func<StringBuilder, int, int, bool> callBuilded;
    static Func<StringBuilder, int, int, bool> callBuildedReal;
    private static bool Append<T>(StringBuilder sb, T i1, T i2, Func<T, T, T> operation)
    {
        sb.Append(operation(i1, i2));
        return true;
    }

    private static Func<StringBuilder, T, T, bool> BuildCallMethod<T>(Func<T, T, T> operation)
    {
        return (sb, i1, i2)=> { sb.Append(operation(i1, i2)); return true; };
    }

    private static int AddMethod(int a, int b)
    {
        return a + b;
    }

    static BenchmarkCallSimple()
    {       

        var x = Expression.Parameter(typeof(int));
        var y = Expression.Parameter(typeof(int));
        var additionExpr = Expression.Add(x, y);

        callLambdaConst = BuildCallMethod<int>(AddMethod);
        callLambda = BuildCallMethod<int>((a, b) => a + b);

        var operationDelegate = Expression.Lambda<Func<int, int, int>>(additionExpr, x, y).Compile();
        callBuilded = BuildCallMethod(operationDelegate);

        var operationExpressionConst = Expression.Constant(operationDelegate, operationDelegate.GetType());

        var sb1 = Expression.Parameter(typeof(StringBuilder), "sb");
        var i1  = Expression.Parameter(typeof(int), "i1");
        var i2  = Expression.Parameter(typeof(int), "i2");
        var appendMethodInfo = typeof(BenchmarkCallSimple).GetTypeInfo().GetDeclaredMethod(nameof(BenchmarkCallSimple.Append));
        var appendMethodInfoGeneric = appendMethodInfo.MakeGenericMethod(typeof(int));
        var appendCallExpression = Expression.Call(appendMethodInfoGeneric,
                new Expression[] { sb1, i1, i2, operationExpressionConst }
            );
        var appendLambda = Expression.Lambda(appendCallExpression, new[] { sb1, i1, i2 });
        callBuildedReal = (Func<StringBuilder, int, int, bool>)(appendLambda.Compile());
    }

    [Benchmark]
    public string CallBuildedReal()
    {
        StringBuilder sb = new StringBuilder();
        var b = callBuildedReal(sb, 1, 2);
        return sb.ToString();
    }

    [Benchmark]
    public string CallBuilded()
    {
        StringBuilder sb = new StringBuilder();
        var b = callBuilded(sb, 1, 2);
        return sb.ToString();
    }

    [Benchmark]
    public string CallLambda()
    {
        StringBuilder sb = new StringBuilder();
        var b = callLambda(sb, 1, 2);
        return sb.ToString();
    }

    [Benchmark]
    public string CallLambdaConst()
    {
        StringBuilder sb = new StringBuilder();
        var b = callLambdaConst(sb, 1, 2);
        return sb.ToString();
    }
}

Результаты:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT


          Method |  Job | Runtime |     Mean |    Error |   StdDev |      Min |      Max |   Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |---------:|---------:|---------:|---------:|---------:|---------:|-----:|-------:|----------:|
 CallBuildedReal |  Clr |     Clr | 137.8 ns | 2.903 ns | 4.255 ns | 133.6 ns | 149.6 ns | 135.6 ns |    7 | 0.0580 |     192 B |
     CallBuilded |  Clr |     Clr | 122.7 ns | 2.068 ns | 1.934 ns | 118.5 ns | 126.2 ns | 122.6 ns |    6 | 0.0576 |     192 B |
      CallLambda |  Clr |     Clr | 119.8 ns | 1.342 ns | 1.255 ns | 117.9 ns | 121.7 ns | 119.6 ns |    5 | 0.0576 |     192 B |
 CallLambdaConst |  Clr |     Clr | 121.7 ns | 1.347 ns | 1.194 ns | 120.1 ns | 124.5 ns | 121.6 ns |    6 | 0.0571 |     192 B |
 CallBuildedReal | Core |    Core | 114.8 ns | 2.263 ns | 2.117 ns | 112.7 ns | 118.8 ns | 113.7 ns |    3 | 0.0594 |     191 B |
     CallBuilded | Core |    Core | 109.0 ns | 1.701 ns | 1.591 ns | 106.5 ns | 112.2 ns | 108.8 ns |    2 | 0.0599 |     191 B |
      CallLambda | Core |    Core | 107.0 ns | 1.181 ns | 1.105 ns | 105.7 ns | 109.4 ns | 106.8 ns |    1 | 0.0593 |     191 B |
 CallLambdaConst | Core |    Core | 117.3 ns | 2.706 ns | 3.704 ns | 113.4 ns | 127.8 ns | 116.0 ns |    4 | 0.0592 |     191 B |

Код теста:

Примечание 1. Существует аналогичный поток SO "Производительность деревьев выражений", в котором выполняется сборка выражение показывает лучший результат в тесте.

Примечание 2: я должен быть близок к тому, чтобы ответить, когда я получу IL-код скомпилированного выражения, поэтому я пытаюсь узнать, как получить IL-код скомпилированного выражения (linqpad ?, ilasm, интегрированный в VS ?, динамическая сборка?), но если вы знаете простой плагин, который умеет это делать из VS - он мне очень поможет.

Примечание 3: это не работает

    var assemblyBuilder = System.AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("testLambda"),System.Reflection.Emit.AssemblyBuilderAccess.Save);
    var modelBuilder = assemblyBuilder.DefineDynamicModule("testLambda_module", "testLambda.dll");
    var typeBuilder = modelBuilder.DefineType("testLambda_type");
    var method = typeBuilder.DefineMethod("testLambda_method", MethodAttributes.Public | MethodAttributes.Static, typeof(bool), 
        new[] { typeof(StringBuilder), typeof(int), typeof(int), typeof(bool) });
    appendLambda.CompileToMethod(method);
    typeBuilder.CreateType();
    assemblyBuilder.Save("testLambda.dll");

Из-за System.TypeInitializationException: «InvalidOperationException: CompileToMethod не может скомпилировать константу» System.Func3[System.Int32,System.Int32,System.Int32]' because it is a non-trivial value, such as a live object. Instead, create an expression tree that can construct this value." That meansappendLambda содержит тип параметра is Func, который не является примитивным типом, и для CompileToMethod существует ограничение на использование только примитивов.


person Roman Pokrovskij    schedule 29.05.2017    source источник
comment
Ваш код не компилируется в текущей форме (есть ссылки на BenchmarkJsonSimple и additionExpr). При микрооптимизации важно иметь компилируемый код.   -  person Evk    schedule 29.05.2017
comment
Спасибо. Я его улучшил. Изменения в последнюю минуту. Теперь код полностью совместим, но я все еще не уверен, что это улучшит ответ. :) Это сейчас массово.   -  person Roman Pokrovskij    schedule 29.05.2017
comment
Чтобы просмотреть сгенерированный IL - создайте модуль во время выполнения, затем используйте Expression.CompileToMethod для его компиляции, затем сохраните модуль на диск и исследуйте IL с помощью обычных инструментов. Что касается кода - всегда лучше, если вы можете скопировать, вставить какой-нибудь код и сразу запустить его.   -  person Evk    schedule 29.05.2017
comment
Я не могу сделать это просто из-за System.TypeInitializationException ... appendLambda содержит параметр Func ‹int, int, int›, который не является типом premeteve, и есть ограничение для CompileToMethod (как я понимаю - все типы должны быть примитивами )   -  person Roman Pokrovskij    schedule 29.05.2017
comment
Скомпилированные деревья выражений медленнее, чем собственный код ... См., Например, stackoverflow.com/questions/29397282/ ... Каждый раз, когда вы обращаетесь к сгенерированному методу, выполняется дополнительная проверка безопасности. Были даже некоторые комментарии прямо в вопросе stackoverflow.com/questions/24802222 /, что вы ответили.   -  person xanatos    schedule 30.05.2017


Ответы (1)


Скомпилированное выражение может работать медленнее из-за причины:

TL;DR;

Вопрос в том, почему скомпилированный делегат работает медленнее, чем делегат, написанный вручную? Expression.Compile создает DynamicMethod и связывает его с анонимной сборкой для запуска в изолированной среде. Это делает безопасным генерирование и выполнение динамического метода частично доверенным кодом, но добавляет некоторые накладные расходы времени выполнения.

Существуют такие инструменты, как FastExpressionCompiler, которые помогают смягчить проблему (отказ от ответственности: я автор < / em>)

Обновление: просмотр IL скомпилированного делегата

  1. Можно получить скомпилированный IL делегата в виде байтового массива:

    var hello = "Hello";
    Expression<Func<string>> getGreetingExpr = () => hello + " me";
    
    var getGreeting = getGreetingExpr.Compile();
    
    var methodBody = getGreeting.Method.GetMethodBody();
    
    var ilBytes = methodBody.GetILAsByteArray();
    
  2. Вам нужен способ проанализировать / прочитать массив и преобразовать его в инструкции и параметры IL.

Жалко, но я не нашел инструментарий или надежного пакета NuGet, которые позволили бы мне это сделать :-(

Вот соответствующий вопрос SO.

Ближайшим инструментом может быть this .

person dadhi    schedule 29.05.2017
comment
Спасибо, ваш проект основан на головокружительной идее, стоящей от меня звезды github. Но у меня все та же головная боль: как лучше всего получить il код скомпилированного дерева выражений? Я бы предпочел получить его прямо в отладчике VS. Что вы для этого используете? - person Roman Pokrovskij; 29.05.2017
comment
Кстати: интересно, что в stackoverflow.com/questions / 24802222 / скомпилированная лямбда сборки работает даже быстрее, чем делегат. Почему? - person Roman Pokrovskij; 29.05.2017
comment
Вы можете скомпилировать динамическую сборку и проверить IL с помощью чего-то вроде dnSpy - person dadhi; 29.05.2017
comment
Может быть, я могу, но как я могу пройти через System.TypeInitializationException: InvalidOperationException: CompileToMethod не может скомпилировать константу 'System.Func`3 [System.Int32, System.Int32, System.Int32]', потому что это нетривиальное значение, например как живой объект. Вместо этого создайте дерево выражения, которое может построить это значение. - person Roman Pokrovskij; 29.05.2017
comment
Хех, это потому, что компиляция в метод не поддерживает замыкание по константам. Итак, вы можете сделать выражение статическим, предоставив константы через параметры. Или декомпилируйте массив байтов тела делегата. - person dadhi; 29.05.2017
comment
Спасибо за помощь. Можете ли вы объяснить, что делает выражение статичным? Я использую MethodAttributes.Static в качестве параметра DefineMethod (код был добавлен к моему вопросу), и это не работает. Какой инструмент может декомпилировать массив байтов тела? А как получить байтовый массив []? Есть способ сериализовать тело метода удаления? Не могу найти :( - person Roman Pokrovskij; 30.05.2017
comment
Обновил мой ответ с образцом получения байтов IL. Под статическим делегатом я имел в виду сделать его эквивалентом статического метода, который не использует какое-либо состояние времени выполнения, то есть чистую / ссылочную прозрачную функцию. - person dadhi; 30.05.2017
comment
Добавлена ​​ссылка на stackoverflow.com/questions/2436082/msil-inspection - person dadhi; 30.05.2017