2016-02-16 10 views
17

Für einen dynamischen binären Übersetzungssimulator muss ich sammelbare .NET-Assemblys mit Klassen generieren, die auf statische Felder zugreifen. Bei Verwendung statischer Felder in sammelbaren Assemblys ist die Ausführungsleistung jedoch im Vergleich zu nicht erfassbaren Assemblys um den Faktor 2-3 niedriger. Dieses Phänomen ist nicht in sammelbaren Baugruppen vorhanden, die keine statischen Felder verwenden.Statischer Feldzugriff in dynamischen Sammelkomponenten fehlt Leistung

Im folgenden Code wird die Methode MyMethod der abstrakten Klasse AbstrTest durch sammelbare und nicht sammelbare dynamische Baugruppen implementiert. Unter Verwendung von CreateTypeConst multipliziert der MyMethod den ulong-Argumentwert mit einem konstanten Wert von zwei, während der zweite Faktor unter Verwendung von CreateTypeField aus einem Konstruktor initialisiertes statisches Feld MyField entnommen wird.

Um realistische Ergebnisse zu erhalten, werden die MyMethod Ergebnisse in einer for-Schleife gesammelt.

Hier sind die Messergebnisse (.NET CLR 4.5/4.6):

Testing non-collectible const multiply: 
Elapsed: 8721.2867 ms 

Testing collectible const multiply: 
Elapsed: 8696.8124 ms 

Testing non-collectible field multiply: 
Elapsed: 10151.6921 ms 

Testing collectible field multiply: 
Elapsed: 33404.4878 ms 

Hier ist mein reproducer Code:

using System; 
using System.Reflection; 
using System.Reflection.Emit; 
using System.Diagnostics; 

public abstract class AbstrTest { 
    public abstract ulong MyMethod(ulong x); 
} 

public class DerivedClassBuilder { 

    private static Type CreateTypeConst(string name, bool collect) { 
    // Create an assembly. 
    AssemblyName myAssemblyName = new AssemblyName(); 
    myAssemblyName.Name = name; 
    AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
     myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run); 

    // Create a dynamic module in Dynamic Assembly. 
    ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name); 

    // Define a public class named "MyClass" in the assembly. 
    TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest)); 

    // Create the MyMethod method. 
    MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod", 
     MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig, 
     typeof(ulong), new Type [] { typeof(ulong) }); 
    ILGenerator methodIL = myMethodBuilder.GetILGenerator(); 
    methodIL.Emit(OpCodes.Ldarg_1); 
    methodIL.Emit(OpCodes.Ldc_I4_2); 
    methodIL.Emit(OpCodes.Conv_U8); 
    methodIL.Emit(OpCodes.Mul); 
    methodIL.Emit(OpCodes.Ret); 

    return myTypeBuilder.CreateType(); 
    } 

    private static Type CreateTypeField(string name, bool collect) { 
    // Create an assembly. 
    AssemblyName myAssemblyName = new AssemblyName(); 
    myAssemblyName.Name = name; 
    AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
     myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run); 

    // Create a dynamic module in Dynamic Assembly. 
    ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name); 

    // Define a public class named "MyClass" in the assembly. 
    TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest)); 

    // Define a private String field named "MyField" in the type. 
    FieldBuilder myFieldBuilder = myTypeBuilder.DefineField("MyField", 
     typeof(ulong), FieldAttributes.Private | FieldAttributes.Static); 

    // Create the constructor. 
    ConstructorBuilder constructor = myTypeBuilder.DefineConstructor(
     MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig, 
     CallingConventions.Standard, Type.EmptyTypes); 
    ConstructorInfo superConstructor = typeof(AbstrTest).GetConstructor(
     BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, 
     null, Type.EmptyTypes, null); 
    ILGenerator constructorIL = constructor.GetILGenerator(); 
    constructorIL.Emit(OpCodes.Ldarg_0); 
    constructorIL.Emit(OpCodes.Call, superConstructor); 
    constructorIL.Emit(OpCodes.Ldc_I4_2); 
    constructorIL.Emit(OpCodes.Conv_U8); 
    constructorIL.Emit(OpCodes.Stsfld, myFieldBuilder); 
    constructorIL.Emit(OpCodes.Ret); 

    // Create the MyMethod method. 
    MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod", 
     MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig, 
     typeof(ulong), new Type [] { typeof(ulong) }); 
    ILGenerator methodIL = myMethodBuilder.GetILGenerator(); 
    methodIL.Emit(OpCodes.Ldarg_1); 
    methodIL.Emit(OpCodes.Ldsfld, myFieldBuilder); 
    methodIL.Emit(OpCodes.Mul); 
    methodIL.Emit(OpCodes.Ret); 

    return myTypeBuilder.CreateType(); 
    } 

    public static void Main() { 
    ulong accu; 
    Stopwatch stopwatch; 
    try { 
     Console.WriteLine("Testing non-collectible const multiply:"); 
     AbstrTest i0 = (AbstrTest)Activator.CreateInstance(
     CreateTypeConst("MyClassModule0", false)); 
     stopwatch = Stopwatch.StartNew(); 
     accu = 0; 
     for (uint i = 0; i < 0xffffffff; i++) 
     accu += i0.MyMethod(i); 
     stopwatch.Stop(); 
     Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms"); 

     Console.WriteLine("Testing collectible const multiply:"); 
     AbstrTest i1 = (AbstrTest)Activator.CreateInstance(
     CreateTypeConst("MyClassModule1", true)); 
     stopwatch = Stopwatch.StartNew(); 
     accu = 0; 
     for (uint i = 0; i < 0xffffffff; i++) 
     accu += i1.MyMethod(i); 
     stopwatch.Stop(); 
     Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms"); 

     Console.WriteLine("Testing non-collectible field multiply:"); 
     AbstrTest i2 = (AbstrTest)Activator.CreateInstance(
     CreateTypeField("MyClassModule2", false)); 
     stopwatch = Stopwatch.StartNew(); 
     accu = 0; 
     for (uint i = 0; i < 0xffffffff; i++) 
     accu += i2.MyMethod(i); 
     stopwatch.Stop(); 
     Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms"); 

     Console.WriteLine("Testing collectible field multiply:"); 
     AbstrTest i3 = (AbstrTest)Activator.CreateInstance(
     CreateTypeField("MyClassModule3", true)); 
     stopwatch = Stopwatch.StartNew(); 
     accu = 0; 
     for (uint i = 0; i < 0xffffffff; i++) 
     accu += i3.MyMethod(i); 
     stopwatch.Stop(); 
     Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms"); 
    } 
    catch (Exception e) { 
     Console.WriteLine("Exception Caught " + e.Message); 
    } 
    } 
} 

Also meine Frage ist: Warum ist es langsamer?

Antwort

12

Ja, das ist eine ziemlich unvermeidliche Konsequenz der Art, wie statische Variablen zugewiesen werden. Ich werde zuerst beschreiben, wie Sie das "Visuelle" zurück in Visual Studio bringen, Sie werden nur eine Chance haben, solche Perf-Probleme zu diagnostizieren, wenn Sie sich den Maschinencode anschauen können, den der Jitter erzeugt.

Das ist schwierig zu tun für Reflection.Emit Code, können Sie nicht durch den Delegat-Aufruf Schritt noch haben Sie eine Möglichkeit, genau zu finden, wo der Code generiert wird. Was Sie tun möchten, ist ein Aufruf von Debugger.Break(), so dass der Debugger an der genau richtigen Stelle stoppt. Also:

ILGenerator methodIL = myMethodBuilder.GetILGenerator(); 
    var brk = typeof(Debugger).GetMethod("Break"); 
    methodIL.Emit(OpCodes.Call, brk); 
    methodIL.Emit(OpCodes.Ldarg_1); 
    // etc.. 

Ändern Sie die Schleife wiederholt auf 1. Extras> Optionen> Debuggen> Allgemein. Deaktivieren Sie "Just My Code" und "JIT-Optimierung unterdrücken". Debug Registerkarte> Haken "Enable code debugging". Wechseln Sie zum Release-Build. Ich poste den 32-Bit-Code, es macht mehr Spaß, da der x64-Jitter einen viel besseren Job machen kann.

Der Maschinencode für die „Testing uneinbringliche mehren“ test wie folgt aussieht:

01410E70 push  dword ptr [ebp+0Ch]  ; Ldarg_1, high 32-bits 
01410E73 push  dword ptr [ebp+8]   ; Ldarg_1, low 32-bits 
01410E76 push  dword ptr ds:[13A6528h] ; myFieldBuilder, high 32-bits 
01410E7C push  dword ptr ds:[13A6524h] ; myFieldBuilder, low 32-bits 
01410E82 call  @[email protected] (73AE1C20h) ; 64 bit multiply 

Nichts sehr drastisch los, ruft sie in eine CLR-Hilfsmethode eine 64-Bit-Multiplikations auszuführen. Der x64-Jitter kann dies mit einem einzigen IMUL-Befehl tun. Beachten Sie den Zugriff auf die statische myFieldBuilder Variable, es hat eine fest codierte Adresse, 0x13A6524. Es wird auf Ihrer Maschine anders sein. Dies ist sehr effizient.

Jetzt die enttäuschend:

059F0480 push  dword ptr [ebp+0Ch]  ; Ldarg_1, high 32-bits 
059F0483 push  dword ptr [ebp+8]   ; Ldarg_1, low 32-bits 
059F0486 mov   ecx,59FC8A0h    ; arg2 = DynamicClassDomainId 
059F048B xor   edx,edx     ; arg1 = DomainId 
059F048D call  JIT_GetSharedNonGCStaticBaseDynamicClass (73E0A6C7h) 
059F0492 push  dword ptr [eax+8]   ; @myFieldBuilder, high 32-bits 
059F0495 push  dword ptr [eax+4]   ; @myFieldBuilder, low 32-bits 
059F0498 call  @[email protected] (73AE1C20h) ; 64-bit multiply 

Sie können sagen, warum es langsamer aus einer halben Meile entfernt, gibt es einen zusätzlichen Anruf JIT_GetSharedNonGCStaticBaseDynamicClass. Es handelt sich um eine Hilfsfunktion in der CLR, die speziell für den Umgang mit statischen Variablen in Reflection.Emit Code entwickelt wurde, der mit AssemblyBuilderAccess.RunAndCollect erstellt wurde. Sie können die Quelle heute sehen, es is here.Lässt alle Augen bluten, aber es ist eine Funktion, die einen AppDomain-Bezeichner und einen dynamischen Klassenbezeichner (auch bekannt als Typ-Handle) einem zugewiesenen Speicherbereich zuordnet, der statische Variablen speichert.

In der "nicht-sammelbaren" Version kennt der Jitter die spezifische Adresse, wo die statische Variable gespeichert ist. Sie hat die Variable zugewiesen, als sie den Code aus einer internen Struktur namens Loader-Heap, die der AppDomain zugeordnet ist, ausgeschieden hat. Wenn die genaue Adresse der Variablen bekannt ist, kann sie direkt die Adresse der Variablen im Maschinencode ausgeben. Sehr effizient natürlich, es gibt keinen Weg, dies schneller zu machen.

Aber das kann nicht in der "Sammler" Version arbeiten, es muss nicht nur Müll sammeln den Maschinencode, sondern auch die statischen Variablen. Dies kann nur funktionieren, wenn der Speicher dynamisch zugewiesen wird. So kann es dynamisch freigegeben werden. Die zusätzliche Indirektion, vergleichen Sie es mit einem Wörterbuch, macht den Code langsamer.

Vielleicht wissen Sie jetzt, warum .NET-Assemblies (und Code) nicht entladen werden können, wenn die AppDomain nicht entladen wird. Es ist eine sehr, sehr wichtige Perf Optimierung.

Nicht sicher, welche Art von Empfehlung Sie erhalten möchten. Man müsste sich selbst um statische Variablenspeicherung kümmern, eine Klasse mit Instanzfeldern. Kein Problem, diese gesammelt zu bekommen. Immer noch nicht so schnell, es braucht eine extra Indirection, aber definitiv schneller, als die CLR sich darum kümmern zu lassen.

+0

Das habe ich im letzten Abschnitt vorgeschlagen. Nicht statische Instanzfelder einer Klasse, Sie haben nur eine Instanz des Klassenobjekts. Sie müssen es in Ihren ILGenerator.Emit() -Aufrufen verwenden. Und verwenden Sie GCHandle.Alloc(), um sicherzustellen, dass es am Leben bleibt, bis der Code gesammelt wird. –

+0

Ich denke, die Kernrestriktion ist, dass der Jitter nicht sicher davon ausgehen kann, dass eine statische Variable zerstört oder zurückgesetzt wird, wenn die Sammeleinheit gesammelt wird. Ein Verweis auf ein Klassenobjekt zu behalten, dessen Code weg ist, ist katastrophal und ausnutzbar. So ähnlich. Das "Warum" wird natürlich nicht hilfreich sein, um Ihr Problem zu lösen. –

+0

Gute Antwort. Danke vielmals. – Paebbels