L'IL généré par les arbres d'expression est-il optimisé?

c# compiler-optimization expression-trees il jit

Question

Ok ceci est simplement de la curiosité, ne sert aucune aide du monde réel.

Je sais qu'avec les arbres d'expression, vous pouvez générer MSIL à la volée, à l'instar du compilateur C # classique. Comme le compilateur peut décider des optimisations, je suis tenté de demander quel est le cas avec IL généré pendant Expression.Compile() . Fondamentalement deux questions:

  1. Étant donné qu’au moment de la compilation, le compilateur peut produire une version différente (peut-être légèrement) de l’IL en mode débogage et en mode libération , existe-t-il une différence dans l’IL généré par la compilation d’une expression lorsqu’il est intégré dans les modes débogage et libération?

  2. De même, JIT qui convertit IL en code natif au moment de l’exécution devrait être très différent en mode débogage et en mode libération. Est-ce aussi le cas des expressions compilées? Ou bien l'IL des arbres d'expression n'est-il pas jeté du tout?

Ma compréhension pourrait être erronée, corrigez-moi au cas où.

Remarque: Je considère les cas où le débogueur est détaché. Je pose des questions sur le paramètre de configuration par défaut fourni avec "debug" et "release" dans Visual Studio.

Réponse acceptée

Étant donné qu’au moment de la compilation, le compilateur peut produire une version différente (peut-être légèrement) de l’IL en mode débogage et en mode libération, existe-t-il une différence dans l’IL généré par la compilation d’une expression lorsqu’il est intégré dans les modes débogage et libération?

Celui-ci a en fait une réponse très simple: non. Étant donné deux arbres d'expression LINQ / DLR identiques, il n'y aura aucune différence dans l'IL généré si l'un est compilé par une application s'exécutant en mode Release et l'autre en mode Debug. Je ne sais pas comment cela serait mis en œuvre de toute façon; Je ne connais aucun moyen fiable pour le code dans System.Core de savoir que votre projet exécute une version de débogage ou une version.

Cette réponse peut toutefois être trompeuse. L'IL émis par le compilateur d'expression peut ne pas différer entre les versions debug et release, mais dans les cas où des arbres d'expression sont émis par le compilateur C #, il est possible que la structure des arbres d'expression eux-mêmes diffère entre les modes debug et release. Je connais assez bien les composants internes de LINQ / DLR, mais pas tellement le compilateur C #, je ne peux donc que dire qu’il peut y avoir une différence (et qu’il peut ne pas en être ainsi).

De même, JIT qui convertit IL en code natif au moment de l’exécution devrait être très différent en mode débogage et en mode libération. Est-ce aussi le cas des expressions compilées? Ou bien l'IL des arbres d'expression n'est-il pas jeté du tout?

Le code machine généré par le compilateur JIT ne sera pas nécessairement très différent pour l'IL pré-optimisé par rapport à l'IL non optimisé. Les résultats peuvent être identiques, en particulier si les seules différences sont quelques valeurs temporaires supplémentaires. Je soupçonne que les deux vont diverger davantage dans les méthodes les plus grandes et les plus complexes, car il existe généralement une limite supérieure au temps / effort que JIT consacrera à l'optimisation d'une méthode donnée. Mais il semble que vous soyez plus intéressé par la qualité des arbres d’expression LINQ / DLR compilés par rapport au code C # compilé en mode débogage ou édition.

Je peux vous dire que le LINQ / DLR LambdaCompiler effectue très peu d'optimisations - moins que le compilateur C # en mode Release, c'est certain; Le mode de débogage est peut-être plus proche, mais mon argent serait légèrement plus agressif sur le compilateur C #. LambdaCompiler ne LambdaCompiler généralement pas de réduire l'utilisation de locaux temporaires, et les opérations telles que les conditions, les comparaisons et les conversions de types utilisent généralement plus de locaux intermédiaires que ce à quoi vous pourriez vous attendre. Je ne fait penser à trois optimisations qu'il ne fonctionne:

  1. Les lambdas imbriqués seront en ligne lorsque cela est possible (et "dans la mesure du possible" a tendance à être "la plupart du temps"). Cela peut aider beaucoup, en fait. Remarque, cela ne fonctionne que lorsque vous Invoke afficher un LambdaExpression ; cela ne s'applique pas si vous appelez un délégué compilé dans votre expression.

  2. Les conversions de type inutiles / redondantes sont omises, du moins dans certains cas.

  3. Si la valeur d'une TypeBinaryExpression (c'est-à-dire que [value] is [Type] ) est connue au moment de la compilation, cette valeur peut être insérée en tant que constante.

Mis à part le n ° 3, le compilateur d'expression ne fait aucune optimisation "basée sur l'expression"; c'est-à-dire qu'il n'analysera pas l'arbre d'expression à la recherche d'opportunités d'optimisation. Les autres optimisations de la liste se produisent avec peu ou pas de contexte par rapport aux autres expressions de l’arbre.

En règle générale, vous devez supposer que l'IL résultant d'une expression LINQ / DLR compilée est considérablement moins optimisé que l'IL produit par le compilateur C #. Cependant, le code IL résultant est éligible pour l'optimisation JIT . Il est donc difficile d'évaluer l'impact sur les performances dans le monde réel à moins que vous n'essayiez réellement de le mesurer avec un code équivalent.

Lors de la composition de code avec des arbres d’expression, vous devez garder à l’esprit que vous êtes le compilateur 1 . Les arbres LINQ / DLR sont conçus pour être émis par une autre infrastructure de compilateur, comme les différentes implémentations du langage DLR. C'est donc à vous de gérer les optimisations au niveau de l'expression. Si vous êtes un compilateur négligent et que vous émettez un tas de code inutile ou redondant, l’IL généré sera plus volumineux et moins susceptible d’être optimisé de manière agressive par le compilateur JIT. Soyez donc attentif aux expressions que vous construisez, mais ne vous inquiétez pas trop. Si vous avez besoin d’une IL hautement optimisée, vous devriez probablement l’émettre vous-même. Mais dans la plupart des cas, les arbres LINQ / DLR fonctionnent très bien.


1 Si vous vous êtes déjà demandé pourquoi les expressions LINQ / DLR sont si ardentes à propos de l'exigence de correspondance de type exacte, c'est parce qu'elles sont destinées à servir de cible de compilation pour plusieurs langues, chacune pouvant avoir des règles différentes en matière de liaison de méthode, de type implicite et explicite. conversions, etc. Par conséquent, lors de la création manuelle d'arborescences LINQ / DLR, vous devez effectuer le travail qu'un compilateur devrait normalement effectuer en arrière-plan, comme l'insertion automatique de code pour les conversions implicites.


Réponse populaire

Squaring un int .

Je ne sais pas si cela se voit beaucoup, mais je suis venu avec l'exemple suivant:

// make delegate and find length of IL:
Func<int, int> f = x => x * x;
Console.WriteLine(f.Method.GetMethodBody().GetILAsByteArray().Length);

// make expression tree
Expression<Func<int, int>> e = x => x * x;

// one approach to finding IL length
var methInf = e.Compile().Method;
var owner = (System.Reflection.Emit.DynamicMethod)methInf.GetType().GetField("m_owner", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(methInf);
Console.WriteLine(owner.GetILGenerator().ILOffset);

// another approach to finding IL length
var an = new System.Reflection.AssemblyName("myTest");
var assem = AppDomain.CurrentDomain.DefineDynamicAssembly(an, System.Reflection.Emit.AssemblyBuilderAccess.RunAndSave);
var module = assem.DefineDynamicModule("myTest");
var type = module.DefineType("myClass");
var methBuilder = type.DefineMethod("myMeth", System.Reflection.MethodAttributes.Static);
e.CompileToMethod(methBuilder);
Console.WriteLine(methBuilder.GetILGenerator().ILOffset);

Résultats:

Dans la configuration Debug, la longueur de la méthode de compilation est de 8 minutes, tandis que celle de la méthode émise est de 4 secondes.

Dans la configuration Release, la méthode de compilation a une longueur de 4, alors que la méthode émise a une longueur de 4.

La méthode de compilation vue par IL DASM en mode débogage:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
}

et libération:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  ret
}

Disclaimer: Je ne suis pas sûr que l'on puisse conclure quelque chose (c'est un long "commentaire"), mais peut-être que Compile() toujours lieu avec des "optimisations"?



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow