2016-02-16 13 views
17

Do symulacji dynamicznego tłumaczenia binarnego potrzebuję generować kolekcjonowalne zestawy .NET z klasami, które uzyskują dostęp do pól statycznych. Jednak w przypadku korzystania z pól statycznych w zespołach kolekcjonerskich wydajność wykonywania jest niższa o 2-3 razy w porównaniu z nieciągliwymi złożeniami. Zjawisko to nie występuje w kolekcjonowalnych złożeniach , które nie używają pól statycznych.Dostęp do pola statycznego w kolekcjonerskich złożeniach dynamicznych jest niewystarczający.

W poniższym kodzie metoda MyMethod abstrakcyjnej klasy AbstrTest jest implementowana przez kolekcjonerskie i nie kolekcjonowalne zestawy dynamiczne. Przy użyciu CreateTypeConst mnoży się wartość argumentu ulong o stałą wartość dwóch, podczas gdy przy użyciu CreateTypeField drugi czynnik jest pobierany z zainicjowanego przez konstruktora pola statycznego MyField. Aby uzyskać realistyczne wyniki, wyniki

są kumulowane w pętli for.

Oto wyniki pomiarów (.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 

Oto mój kod reproducer:

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

Więc moje pytanie brzmi: Dlaczego wolniej?

Odpowiedz

12

Tak, jest to nieunikniona konsekwencja sposobu alokacji zmiennych statycznych. Najpierw opiszę, w jaki sposób wstawiasz "visual" z powrotem do Visual Studio, będziesz miał tylko szansę na zdiagnozowanie problemów z perfekcją, kiedy będziesz mógł spojrzeć na kod maszynowy wygenerowany przez jittera.

To jest trudne do zrobienia dla kodu Reflection.Emit, nie można przejść przez połączenie delegata ani nie masz sposobu, aby znaleźć dokładnie, gdzie generowany jest kod. To, co chcesz zrobić, to wstrzyknąć wywołanie Debugger.Break(), aby debugger zatrzymał się dokładnie w odpowiednim miejscu. A więc:

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

Zmiana powtórzeń pętli na 1. Narzędzia> Opcje> Debugowanie> Ogólne. Usuń zaznaczenie "Just My Code" i "Suppress Optimization JIT". Karta Debugowanie> zaznacz "Włącz debugowanie kodu natywnego". Przejdź do wersji Release. Opublikuję 32-bitowy kod, to jest fajniejsze, ponieważ jitter x64 może zrobić o wiele lepszą pracę.

kodu maszynowego dla „testing non-kolekcjonowania dziedzinie mnożyć” test wygląda następująco:

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 

Nic bardzo drastyczne dzieje, to wzywa do metody CLR pomocnika do wykonywania 64-bitowym mnożyć. Jitter x64 może to zrobić za pomocą pojedynczej instrukcji IMUL. Zwróć uwagę na dostęp do zmiennej statycznej myFieldBuilder, ma ona zakodowany adres, 0x13A6524. Będzie inaczej na twoim komputerze. Jest to bardzo wydajne.

Teraz rozczarowujących jednym:

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 

Można powiedzieć, dlaczego to wolniejsze od pół mili dalej, tam dodatkowe wezwanie do JIT_GetSharedNonGCStaticBaseDynamicClass. Jest to funkcja pomocnicza wewnątrz CLR, która została specjalnie zaprojektowana do obsługi zmiennych statycznych używanych w kodzie Reflection.Emit, który został zbudowany przy użyciu AssemblyBuilderAccess.RunAndCollect. Możesz zobaczyć źródło dzisiaj, to is here.Sprawia, że ​​oczy wszystkich krwawią, ale funkcja odwzorowuje identyfikator AppDomain i dynamiczny identyfikator klasy (znany także jako uchwyt typu) na przydzielony fragment pamięci przechowującej zmienne statyczne.

W wersji "nie do kolekcjonowania" jitter zna konkretny adres, w którym zapisana jest zmienna statyczna. Przydzielił on zmienną, gdy podskoczył kod z wewnętrznej struktury zwanej "stertą ładowacza", powiązanej z AppDomain. Znając dokładny adres zmiennej, może bezpośrednio emitować adres zmiennej w kodzie maszynowym. Oczywiście bardzo wydajne, nie ma możliwości, aby zrobić to szybciej.

Ale to nie działa w wersji "kolekcjonerskiej", to nie tylko musi marnować kolekcjonowanie kodu maszynowego, ale również zmienne statyczne:. To może działać tylko wtedy, gdy pamięć jest przydzielana dynamicznie. Więc może być dynamicznie uwalniany. Dodatkowy kierunek, porównywalny ze Słownikiem, powoduje, że kod jest wolniejszy.

Być może teraz docenisz powód, dla którego zespołów .NET (i kodu) nie można zwolnić, chyba że AppDomain jest rozładowany. Jest to bardzo, bardzo ważna optymalizacja perfekcji.

Nie jestem pewien, jakiego rodzaju zalecenia chcesz osiągnąć. Jednym z nich byłby dbanie o statyczne magazynowanie zmiennych samemu, klasa z polami instancji. Nie ma problemu ze zdobyciem tych zebranych. Nadal nie będzie tak szybki, wymaga dodatkowego pośrednictwa, ale zdecydowanie szybszego niż pozwolenie, by CLR się tym zajął.

+0

To właśnie zaproponowałem w ostatnim akapicie. Nie statyczne, pola instancji klasy, będziesz mieć tylko jedno wystąpienie obiektu klasy. Będziesz musiał użyć go w swoich wywołaniach ILGenerator.Emit(). I użyj GCHandle.Alloc(), aby upewnić się, że pozostanie przy życiu, dopóki kod nie zostanie zebrany. –

+0

Myślę, że ograniczenie rdzenia jest takie, że jitter nie może bezpiecznie założyć, że zmienna statyczna zostanie zniszczona lub zresetowana po zebraniu zespołu kolekcjonerskiego. Pozostawienie odniesienia do obiektu klasy, którego kod zniknął, jest katastrofalne i możliwe do wykorzystania. Coś w tym stylu. "Dlaczego" nie pomoże, oczywiście, w rozwiązaniu twojego problemu. –

+0

Dobra odpowiedź. Wielkie dzięki. – Paebbels

Powiązane problemy