2016-02-16 94 views
17

Per un simulatore dinamico di traduzione binaria, ho bisogno di generare assiemi .NET da collezione con classi che accedono ai campi statici. Tuttavia, quando si utilizzano campi statici all'interno di gruppi raccoglibili, le prestazioni di esecuzione sono inferiori di 2-3 volte rispetto agli assiemi non raccoglibili. Questo fenomeno non è presente negli assembly da collezione che non utilizzano campi statici.L'accesso al campo statico negli assiemi dinamici collezionabili non ha prestazioni

Nel codice riportato di seguito il metodo MyMethod della classe astratta AbstrTest è implementato da assiemi mobili non raccoglibili e non raccoglibili. Utilizzando CreateTypeConst il MyMethod moltiplica il valore dell'argomento ulong di un valore costante di due, mentre si utilizza CreateTypeField il secondo fattore viene preso da un campo statico inizializzato dal costruttore MyField.

Per ottenere risultati realistici, i risultati MyMethod vengono accumulati in un ciclo for.

Ecco i risultati di misura (CLR .NET 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 

Ecco il mio codice riproduttore:

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

Quindi la mia domanda è: perché è è più lento?

risposta

12

Sì, questa è una conseguenza piuttosto inevitabile del modo in cui vengono allocate le variabili statiche. Descriverò in primo luogo come si inserisce il "visual" in Visual Studio, si avrà solo una possibilità di diagnosticare problemi di perfusione come questo quando si può osservare il codice macchina generato dal jitter.

Questo è difficile da fare per Reflection.Emit codice, non è possibile passare attraverso la chiamata delegata né avete alcun modo per trovare esattamente dove viene generato il codice. Quello che vuoi fare è iniettare una chiamata a Debugger.Break() in modo che il debugger si fermi nel punto esatto. Quindi:

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

Modificare le ripetizioni del ciclo su 1. Strumenti> Opzioni> Debug> Generale. Deseleziona "Solo il mio codice" e "Elimina l'ottimizzazione JIT". Scheda Debug> selezionare "Abilita debug del codice nativo". Passa alla build Release. Pubblicherò il codice a 32 bit, è più divertente dal momento che il jitter x64 può fare un lavoro molto migliore.

Il codice macchina per il "campo non collezionabile Testing moltiplicare" test assomiglia:

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 

Niente di molto drastico in corso, si chiama in un metodo CLR di supporto per eseguire un 64-bit si moltiplicano. Il jitter x64 può farlo con una singola istruzione IMUL. Annoti l'accesso alla variabile statica myFieldBuilder, ha un indirizzo hard-coded, 0x13A6524. Sarà diverso sulla tua macchina. Questo è molto efficiente.

Ora il deludente uno:

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 

Si può dire perché è più lento da mezzo miglio di distanza, c'è una chiamata in più per JIT_GetSharedNonGCStaticBaseDynamicClass. È una funzione di supporto all'interno del CLR specificamente progettata per gestire le variabili statiche utilizzate nel codice Reflection.Emit creato con AssemblyBuilderAccess.RunAndCollect. Puoi vedere la fonte oggi, è is here.Fa sanguinare gli occhi di tutti, ma è una funzione che associa un identificatore di AppDomain e un identificatore di classe dinamico (noto anche come handle di tipo) a un pezzo di memoria allocato che memorizza le variabili statiche.

Nella versione "non raccoglibile" il jitter conosce l'indirizzo specifico in cui è memorizzata la variabile statica. Assegnava la variabile quando eseguiva il codice da una struttura interna chiamata "heap del caricatore", associata all'AppDomain. Conoscendo l'indirizzo esatto della variabile, può emettere direttamente l'indirizzo della variabile nel codice macchina. Ovviamente molto efficiente, non c'è modo di farlo più velocemente.

Ma questo non può funzionare nella versione "da collezione", non è solo necessario raccogliere i rifiuti del codice macchina ma anche le variabili statiche. Questo può funzionare solo quando lo spazio di archiviazione viene assegnato dinamicamente. Quindi può essere rilasciato dinamicamente. L'extraindirizzamento, confrontalo con un dizionario, è ciò che rende il codice più lento.

Probabilmente ora apprezzerai il motivo per cui gli assembly .NET (e il codice) non possono essere scaricati a meno che AppDomain non sia scaricato. È un'ottimizzazione del perimetro molto, molto importante.

Non so quale tipo di raccomandazione desideri portare avanti. Uno sarebbe prendersi cura della memoria delle variabili statiche, una classe con campi di istanza. Nessun problema a ottenere quelli raccolti. Ancora non sarà così veloce, ci vorrà un'ulteriore indiretta, ma sicuramente più veloce di lasciare che il CLR si prenda cura di esso.

+0

Questo è quello che ho proposto nell'ultimo paragrafo. Non statici, campi di istanza di una classe, avrai solo un'istanza dell'oggetto classe. Dovrai usarlo nelle tue chiamate ILGenerator.Emit(). E usa GCHandle.Alloc() per assicurarti che rimanga in vita fino a quando il codice non viene raccolto. –

+0

Penso che la restrizione principale sia che il jitter non può presumere in modo sicuro che una variabile statica verrà distrutta o resettata quando l'assieme raccoglibile viene raccolto. Aver conservato un riferimento a un oggetto di classe il cui codice è scomparso è disastroso e sfruttabile. Qualcosa del genere. Il "perché" non sarà di aiuto per risolvere il tuo problema, naturalmente. –

+0

Buona risposta. Molte grazie. – Paebbels