c# - requiere - El acceso al campo estático en ensamblajes dinámicos coleccionables carece de rendimiento
enlace estatico y dinamico java (1)
Para un simulador dinámico de traducción binaria, necesito generar ensamblajes .NET coleccionables con clases que tengan acceso a campos estáticos. Sin embargo, cuando se usan campos estáticos dentro de ensamblajes coleccionables, el rendimiento de ejecución es un factor de 2-3 más bajo en comparación con los ensamblajes no coleccionables. Este fenómeno no está presente en ensamblajes coleccionables que no usan campos estáticos.
En el siguiente código, el método MyMethod
de la clase abstracta AbstrTest
se implementa mediante ensamblajes dinámicos de colección y no de colección. Al usar CreateTypeConst
MyMethod
multiplica el valor del argumento ulong por un valor constante de dos, mientras que al usar CreateTypeField
el segundo factor se toma de un campo estático inicializado por el constructor MyField
.
Para obtener resultados realistas, los resultados de MyMethod
se acumulan en un bucle for.
Aquí están los resultados de la medición (.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
Aquí está mi código de reproductor:
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);
}
}
}
Así que mi pregunta es: ¿Por qué es más lento?
Sí, esta es una consecuencia bastante inevitable de la forma en que se asignan las variables estáticas. Primero, describiré cómo vuelve a colocar "visual" en Visual Studio, solo tendrá una oportunidad de diagnosticar problemas de perfección como este cuando pueda ver el código de máquina que genera el jitter.
Eso es difícil de hacer para Reflection. Emita el código, no puede pasar por la llamada de delegado ni tiene una manera de encontrar exactamente dónde se genera el código. Lo que desea hacer es inyectar una llamada a Debugger.Break () para que el depurador se detenga exactamente en el lugar correcto. Asi que:
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
var brk = typeof(Debugger).GetMethod("Break");
methodIL.Emit(OpCodes.Call, brk);
methodIL.Emit(OpCodes.Ldarg_1);
// etc..
Cambie las repeticiones de bucle a 1. Herramientas> Opciones> Depuración> General. Desmarque "Just My Code" y "Suppress JIT optimization". Pestaña Depuración> marque "Habilitar depuración de código nativo". Cambia a la versión de lanzamiento. Publicaré el código de 32 bits, es más divertido ya que el jitter x64 puede hacer un trabajo mucho mejor.
El código de máquina para la prueba "Prueba de multiplicación de campo no coleccionable" se ve así:
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 @JIT_LMul@16 (73AE1C20h) ; 64 bit multiply
No pasa nada muy drástico, llama a un método de ayuda CLR para realizar una multiplicación de 64 bits. El jitter x64 puede hacerlo con una sola instrucción IMUL. Tenga en cuenta el acceso a la variable estática myFieldBuilder
, tiene una dirección codificada, 0x13A6524. Será diferente en tu máquina. Esto es muy eficiente.
Ahora la decepcionante:
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 @JIT_LMul@16 (73AE1C20h) ; 64-bit multiply
Puedes saber por qué es más lento a media milla de distancia, hay una llamada adicional a JIT_GetSharedNonGCStaticBaseDynamicClass. Es una función auxiliar dentro del CLR que se diseñó específicamente para tratar las variables estáticas utilizadas en Reflection.Emit el código que se creó con AssemblyBuilderAccess.RunAndCollect. Puedes ver la fuente hoy, está aquí . Hace sangrar los ojos a todos, pero es una función que asigna un identificador de dominio de aplicación y un identificador de clase dinámico (también conocido como el identificador de tipo) a un pedazo de memoria asignado que almacena variables estáticas.
En la versión "no coleccionable", el jitter conoce la dirección específica donde se almacena la variable estática. Asignó la variable cuando eliminó el código de una estructura interna llamada "montón de cargador", asociada con el dominio de aplicación. Conociendo la dirección exacta de la variable, puede emitir directamente la dirección de la variable en el código de la máquina. Por supuesto, muy eficiente, no hay forma de hacerlo más rápido.
Pero eso no puede funcionar en la versión "coleccionable", no solo tiene que recolectar el código de la máquina, sino también las variables estáticas. Eso solo puede funcionar cuando el almacenamiento se asigna dinámicamente. Así se puede liberar dinámicamente. La indirección adicional, compararlo con un Diccionario, es lo que hace que el código sea más lento.
Quizás ahora aprecie la razón por la que los ensamblados .NET (y el código) no se pueden descargar a menos que se descargue el dominio de aplicación. Es una optimización de perf muy, muy importante.
No estoy seguro de qué tipo de recomendación le gustaría seguir adelante. Uno sería cuidar del almacenamiento de variables estáticas, una clase con campos de instancia. No hay problema en conseguir los recogidos. Aún no será tan rápido, requiere una indirección adicional, pero definitivamente más rápido que dejar que el CLR se ocupe de ello.