valid - documentation code c#
Múltiples tipos en un conjunto dinámico es mucho más lento que múltiples conjuntos dinámicos con un tipo cada uno (2)
En mi PC en LINQPad usando C # 7.0, obtengo un ensamblaje de aproximadamente 8.8 segundos, un ensamblaje por tipo de aproximadamente 2.6 segundos. La mayor parte del tiempo en un conjunto está en DefineType
y CreateType
mientras que en el tiempo está principalmente en DefineDynamicAssembly
+ DefineDynamicModule
.
DefineType
comprueba que no haya conflictos de nombre, lo cual es una búsqueda en el Dictionary
. Si el Dictionary
está vacío, se trata de una comprobación de null
.
La mayor parte del tiempo se invierte en CreateType
, pero no veo dónde, sin embargo, parece que requiere tiempo adicional para agregar tipos a un solo módulo.
La creación de varios módulos ralentiza todo el proceso, pero la mayor parte del tiempo se dedica a crear módulos y en DefineType
, que tiene que escanear cada módulo en busca de un duplicado, por lo que ahora se incrementan hasta 10,000 cheques null
. Con un módulo único por tipo, CreateType
es muy rápido.
Así que estoy emitiendo algunos proxies dinámicos a través de DefineDynamicAssembly
, y mientras probaba encontré que:
- Un tipo por ensamblaje dinámico: rápido, pero usa mucha memoria
- Todos los tipos en un conjunto dinámico: muy, muy lento, pero usa mucho menos memoria
En mi prueba, genero 10,000 tipos y el código de un tipo por ensamblado se ejecuta entre 8 y 10 veces más rápido. El uso de la memoria está completamente en línea con lo que esperaba, pero ¿por qué el tiempo para generar los tipos es mucho más largo?
Edición: Añadido un código de muestra.
Una asamblea:
var an = new AssemblyName( "Foo" );
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( an, AssemblyBuilderAccess.Run );
var mb = ab.DefineDynamicModule( "Bar" );
for( int i = 0; i < 10000; i++ )
{
var tb = mb.DefineType( "Baz" + i.ToString( "000" ) );
var met = tb.DefineMethod( "Qux", MethodAttributes.Public );
met.SetReturnType( typeof( int ) );
var ilg = met.GetILGenerator();
ilg.Emit( OpCodes.Ldc_I4, 4711 );
ilg.Emit( OpCodes.Ret );
tb.CreateType();
}
Un montaje por tipo:
for( int i = 0; i < 10000; i++ )
{
var an = new AssemblyName( "Foo" );
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( an,
AssemblyBuilderAccess.Run );
var mb = ab.DefineDynamicModule( "Bar" );
var tb = mb.DefineType( "Baz" + i.ToString( "000" ) );
var met = tb.DefineMethod( "Qux", MethodAttributes.Public );
met.SetReturnType( typeof( int ) );
var ilg = met.GetILGenerator();
ilg.Emit( OpCodes.Ldc_I4, 4711 );
ilg.Emit( OpCodes.Ret );
tb.CreateType();
}
En mis comprobaciones sobre por qué la definición de varios módulos en un conjunto es más lenta que crear un nuevo conjunto con un módulo, utilizando estos códigos:
Escenario de montaje único:
var an = new AssemblyName("Foo");
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
for (int i = 0; i < 10000; i++)
{
ab.DefineDynamicModule("Bar" + i.ToString("000"));
}
Escenario multi-montaje:
var an = new AssemblyName("Foo");
for (int i = 0; i < 10000; i++)
{
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
ab.DefineDynamicModule("Bar");
}
- Descubrí que alrededor del 20% (50% en varios ejemplos de ensamblajes) de las veces, el código subyacente pasa por todos los nombres de los módulos para verificar cualquier conflicto. Esta parte es comprensible y esperada.
- Cuando se usa un ensamblaje, otro 60% -80% de las veces,
DefineDynamicModule()
CLI está bajo presión. Sin embargo, cuando se usan varios ensamblajes, este método nunca se llama; En cambio, otros métodos son responsables del 50% restante.
Vamos a profundizar en la documentación de ECMA-335 para CLI.
II.6 Un ensamblaje es un conjunto de uno o más archivos implementados como una unidad.
Página 140
Entonces entendemos que un ensamblaje es esencialmente un paquete y los módulos son los componentes principales. Habiendo dicho eso:
II.6 Un módulo es un archivo único que contiene contenido ejecutable en el formato especificado aquí. Si el módulo contiene un manifiesto, también especifica los módulos (incluido él mismo) que constituyen el ensamblaje. Un ensamblaje contendrá solo un manifiesto entre todos sus archivos constituyentes.
Página 140
Sobre la base de esta información, sabemos que cuando creamos el ensamblaje, también agregamos automáticamente un módulo al ensamblaje. Esta es la razón por la que nunca obtenemos un impacto en la función DefineDynamicModule()
CLI si seguimos creando nuevos ensamblajes. En su lugar, recibimos un hit en el método GetInMemoryAssemblyModule()
la CLI para recuperar información sobre el Módulo de manifiesto (el módulo que se crea automáticamente).
Así que aquí tenemos un poco de ganancia de rendimiento; con un conjunto, obtenemos 10001 módulos, pero con varios conjuntos, obtenemos un total de 10000 módulos. No mucho, sin embargo, por lo que este módulo adicional no debería ser la razón principal detrás de esto.
II.6.5 Cuando un elemento se encuentra en el conjunto actual, pero forma parte de un módulo distinto al que contiene el manifiesto, el módulo de definición se declarará en el manifiesto del conjunto utilizando la directiva externa .module.
Página 146
y
II.6.7 El módulo de manifiesto, del cual solo puede haber uno por ensamblaje, incluye la directiva .assembly. Para exportar un tipo definido en cualquier otro módulo de un ensamblaje se requiere una entrada en el manifiesto del ensamblaje.
Página 146
Por lo tanto, cada vez que crea un nuevo módulo, en realidad agrega un nuevo archivo a un archivo y luego modifica el primer archivo del archivo para hacer referencia al nuevo módulo. Esencialmente en el código de ensamblaje único, estamos agregando 10000 módulos, y luego editamos el primer módulo 10000 veces. Este no es el caso con el código de ensamblaje múltiple en el que solo editamos el primer módulo generado automáticamente, 10000 veces.
Esta es la sobrecarga que vemos. Y aumenta exponencialmente en mi sistema.
(5000 = 1.5s, 10000 = 6s, 20000 = 25s)
Sin embargo, con su código, el cuello de botella es la función SetMethodIL
de CLR no administrada llamada desde el método CreateTypeNoLock.CreateTypeNoLock()
y todavía no pude encontrar nada en la documentación sobre esto.
Desafortunadamente, sin embargo, es difícil descompilar y entender CLR.dll para ver qué sucede realmente allí y, como resultado, solo estamos haciendo suposiciones basadas en la información pública publicada por Microsoft en esta etapa.