Wird IL von Expression Trees optimiert?

c# compiler-optimization expression-trees il jit

Frage

Ok das ist nur Neugierde, dient keiner wirklichen Welthilfe.

Ich weiß, dass Sie mit Ausdrucksbäumen MSIL im laufenden Betrieb erzeugen können, genau wie der normale C # -Compiler. Da Compiler Optimierungen entscheiden kann, bin ich versucht zu fragen, was ist der Fall mit IL generiert während Expression.Compile() . Grundsätzlich zwei Fragen:

  1. Da der Compiler während der Kompilierungszeit unterschiedliche (möglicherweise etwas) IL im Debug- und im Release-Modus erzeugen kann, gibt es je einen Unterschied in der IL, die beim Kompilieren eines Ausdrucks erzeugt wird, wenn er im Debug-Modus und im Release-Modus aufgebaut ist?

  2. Auch JIT, die IL zur Laufzeit in nativen Code umwandeln, sollte sowohl im Debug- als auch im Release-Modus sehr unterschiedlich sein. Ist dies auch bei kompilierten Ausdrücken der Fall? Oder sind IL von Ausdrucksbäumen überhaupt nicht verkrustet?

Mein Verständnis könnte fehlerhaft sein, korrigiere mich für den Fall.

Hinweis: Ich denke über die Fälle nach, in denen der Debugger getrennt ist. Ich frage nach der Standardkonfiguration, die mit "debug" und "release" in Visual Studio geliefert wird.

Akzeptierte Antwort

Da der Compiler während der Kompilierungszeit unterschiedliche (möglicherweise etwas) IL im Debug- und im Release-Modus erzeugen kann, gibt es je einen Unterschied in der IL, die beim Kompilieren eines Ausdrucks erzeugt wird, wenn er im Debug-Modus und im Release-Modus aufgebaut ist?

Dieser hat eigentlich eine sehr einfache Antwort: nein. Wenn zwei identische LINQ / DLR-Ausdrucksbäume vorhanden sind, gibt es keinen Unterschied in der generierten IL, wenn einer von einer Anwendung kompiliert wird, die im Freigabemodus und die andere im Debugmodus ausgeführt wird. Ich bin nicht sicher, wie das sowieso umgesetzt werden würde; Ich kenne keinen zuverlässigen Weg für Code innerhalb von System.Core zu wissen, dass Ihr Projekt einen Debug Build oder Release Build ausführt.

Diese Antwort kann jedoch irreführend sein. Die vom Ausdruckscompiler ausgegebene IL darf sich nicht zwischen Debug- und Release-Build unterscheiden, aber in Fällen, in denen Ausdrucksbäume vom C # -Compiler ausgegeben werden, ist es möglich, dass die Struktur der Ausdrucksbäume selbst zwischen Debug- und Release-Modus differiert. Ich bin ziemlich gut mit dem LINQ / DLR Interna kennen, aber nicht so sehr mit dem C # -Compiler, so kann ich nur sagen , dass es ein Unterschied in diesen Fällen sein kann (und es auch nicht).

Auch JIT, die IL zur Laufzeit in nativen Code umwandeln, sollte sowohl im Debug- als auch im Release-Modus sehr unterschiedlich sein. Ist dies auch bei kompilierten Ausdrücken der Fall? Oder sind IL von Ausdrucksbäumen überhaupt nicht verkrustet?

Der Maschinencode, der der JIT - Compiler spuckt nicht notwendigerweise ganz anders sein für IL voroptimierten im Vergleich zu nicht optimierten IL. Die Ergebnisse können durchaus identisch sein, insbesondere wenn die einzigen Unterschiede ein paar zusätzliche temporäre Werte sind. Ich vermute, dass die beiden in größeren und komplexeren Methoden mehr auseinander gehen werden, da es normalerweise eine Obergrenze für die Zeit / den Aufwand gibt, die das JIT für die Optimierung einer bestimmten Methode aufwendet. Aber es scheint, als ob Sie mehr daran interessiert sind, wie die Qualität der kompilierten LINQ / DLR-Ausdrucksbäume mit, sagen wir, im Debug- oder Release-Modus kompiliertem C # -Code verglichen wird.

Ich kann Ihnen sagen, dass der LINQ / DLR LambdaCompiler sehr wenige Optimierungen durchführt - weniger als der C # -Compiler im Release-Modus; Debug-Modus kann näher sein, aber ich würde mein Geld auf den C # -Compiler etwas aggressiver setzen. Der LambdaCompiler Allgemeinen nicht, die Verwendung von temporären Locals zu reduzieren, und Operationen wie Konditionalitäts-, Vergleichs- und Typkonvertierungen verwenden normalerweise mehr temporäre Locals als erwartet. Ich kann eigentlich nur denken Sie an drei Optimierungen , die es ausführen tut:

  1. Geschachtelte lambdas werden, wenn möglich, inline angezeigt (und "wenn möglich" tendiert dazu, "die meiste Zeit" zu sein). Das kann sehr viel helfen. Beachten Sie , dass dies nur funktioniert , wenn Sie Invoke eine LambdaExpression ; Es gilt nicht, wenn Sie einen kompilierten Delegaten in Ihrem Ausdruck aufrufen.

  2. Unnötige / redundante Typumwandlungen entfallen zumindest in einigen Fällen.

  3. Wenn der Wert einer TypeBinaryExpression (dh [value] is [Type] ) zum Zeitpunkt der Kompilierung bekannt ist, kann dieser Wert als Konstante inline sein.

Abgesehen von # 3 führt der Ausdruckcompiler keine "Ausdruck-basierten" Optimierungen durch; Das heißt, es wird nicht der Ausdrucksbaum analysiert, der nach Optimierungsmöglichkeiten sucht. Die anderen Optimierungen in der Liste treten mit wenig oder keinem Kontext über andere Ausdrücke in der Struktur auf.

Im Allgemeinen sollten Sie davon ausgehen, dass die IL, die aus einem kompilierten LINQ / DLR-Ausdruck resultiert, wesentlich weniger optimiert ist als die IL, die vom C # -Compiler erzeugt wird. Der resultierende IL-Code ist jedoch für eine JIT-Optimierung geeignet. Daher ist es schwierig, die Auswirkungen auf die reale Weltleistung zu bewerten, es sei denn, Sie versuchen tatsächlich, sie mit äquivalentem Code zu messen.

Eines der Dinge, die Sie beim Erstellen von Code mit Ausdrucksbäumen beachten sollten, ist, dass Sie tatsächlich der Compiler 1 sind . LINQ / DLR-Bäume sollen von einer anderen Compiler-Infrastruktur wie den verschiedenen DLR-Sprachimplementierungen ausgegeben werden. Es liegt daher an Ihnen , Optimierungen auf Ausdrucksebene zu behandeln. Wenn Sie ein schlampiger Compiler sind und eine Menge unnötigen oder redundanten Code ausgeben, wird die generierte IL größer und weniger wahrscheinlich vom JIT-Compiler aggressiv optimiert. Achte also auf die Ausdrücke, die du konstruierst, aber sorge dich nicht zu sehr. Wenn Sie hoch optimierte IL benötigen, sollten Sie es wahrscheinlich nur selbst emittieren. In den meisten Fällen funktionieren LINQ / DLR-Bäume jedoch gut.


1 Wenn Sie sich jemals gefragt haben, warum LINQ / DLR-Ausdrücke so pedantisch sind, dass ein genaues Typ-Matching erforderlich ist, liegt das daran, dass sie als Compiler-Ziel für mehrere Sprachen dienen, von denen jede unterschiedliche Regeln bezüglich Methodenbindung, implizitem und explizitem Typ hat Konvertierungen usw. Daher müssen Sie beim manuellen Erstellen von LINQ / DLR-Bäumen die Arbeit ausführen, die ein Compiler normalerweise hinter den Kulissen ausführen würde, wie das automatische Einfügen von Code für implizite Konvertierungen.


Beliebte Antwort

Quadratur eines int .

Ich bin nicht sicher, ob das sehr viel zeigt, aber ich habe folgendes Beispiel gefunden:

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

Ergebnisse:

In der Debug-Konfiguration beträgt die Länge der Kompilierzeitmethode 8, während die ausgegebene Methode die Länge 4 hat.

In der Release-Konfiguration beträgt die Länge der Kompilierzeitmethode 4, während die Länge der ausgegebenen Methode ebenfalls 4 ist.

Die Methode zur Kompilierungszeit, wie sie von IL DASM im Debug-Modus erkannt wird:

.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
}

und Veröffentlichung:

.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: Ich bin mir nicht sicher, ob man etwas abschließen kann (das ist ein langer "Kommentar"), aber vielleicht findet der Compile() immer mit "Optimierungen" statt?



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow