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.