3 votes

Pourquoi la compilation de lambda build sur Expression.Call est légèrement plus lente que le délégué censé faire la même chose?

Pourquoi la construction lambda compilée sur Expression.Call est légèrement plus lente que le délégué qui devrait faire la même chose? Et comment l'éviter?

Explication des résultats de BenchmarkDotNet. Nous comparons CallBuildedReal vs CallLambda; deux autres CallBuilded et CallLambdaConst sont des "sous-formes" de CallLambda et montrent des nombres égaux. Mais la différence avec CallBuildedReal est significative.

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

    private static Func BuildCallMethod(Func 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(AddMethod);
        callLambda = BuildCallMethod((a, b) => a + b);

        var operationDelegate = Expression.Lambda>(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)(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();
    }
}

Résultats:

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 |     Moyenne |    Erreur |   DévStd |      Min |      Max |   Médiane | Classement |  Gen 0 | Alloué |
---------------- |----- |-------- |-----------:|---------:|---------:|---------:|---------:|---------:|-----------:|-------:|--------:|
 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 |

Code de Benchmark:

Note 1: il existe un thread SO similaire "Performance of expression trees" où la construction d'expression montre le meilleur résultat dans le benchmark.

Note 2: Je devrais être proche de la réponse lorsque j'aurai le code IL de l'expression compilée, donc j'essaie d'apprendre comment obtenir le code IL de l'expression compilée (linqpad?, ilasm intégré à VS?, assemblage dynamique?), mais si vous connaissez un plugin simple qui peut le faire depuis VS - cela m'aiderait beaucoup.

Note 3: cela ne fonctionne pas

    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");

En raison de l' Exception de TypeInitializationException: "InvalidOperationException: CompileToMethod ne peut pas compiler la constante 'System.Func3[System.Int32,System.Int32,System.Int32]' car c'est une valeur non triviale, comme un objet en direct. Au lieu de cela, créez un arbre d'expression qui peut construire cette valeur." Cela signifie que appendLambda` contient un type de paramètre de Func qui n'est pas un type primitif et qu'il y a une limitation pour CompileToMethod d'utiliser uniquement des types primitifs.

4voto

dadhi Points 11

L'expression compilée peut être plus lente en raison des raisons:

TL;DR;

La question est, pourquoi le delegate compilé est-il beaucoup plus lent qu'un delegate écrit manuellement? Expression.Compile crée une DynamicMethod et l'associe à une assembly anonyme pour l'exécuter dans un environnement sandboxé. Cela permet à une méthode dynamique d'être émise et exécutée par du code partiellement fiable, mais ajoute un surcoût au temps d'exécution.

Il existe des outils comme FastExpressionCompiler qui aident à atténuer le problème (attention : je suis l'auteur)

Mise à jour: Afficher le code IL du delegate compilé

  1. Il est possible d'obtenir le code IL du delegate compilé sous forme de tableau d'octets:

    var hello = "Hello";
    Expression> getGreetingExpr = () => hello + " me";
    
    var getGreeting = getGreetingExpr.Compile();
    
    var methodBody = getGreeting.Method.GetMethodBody();
    
    var ilBytes = methodBody.GetILAsByteArray();
  2. Il faut trouver un moyen de parser/lire le tableau et de le convertir en instructions et paramètres IL.

Dommage, je n'ai pas trouvé d'outil ou de package NuGet robuste permettant de le faire :-(

Voici la question SO associée.

L'outil le plus proche pourrait être celui-ci.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X