example - remarks c#
Rendimiento de la expresión compilado a delegado (5)
Consulte estos enlaces para ver qué sucede cuando compila su LambdaExpression
(y sí, se hace usando Reflection)
Estoy generando un árbol de expresiones que mapea las propiedades de un objeto fuente a un objeto de destino, que luego se compila en Func<TSource, TDestination, TDestination>
y se ejecuta.
Esta es la vista de depuración de LambdaExpression
resultante:
.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
MemberMapper.Benchmarks.Program+ComplexSourceType $right,
MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
.Block(
MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
$left.ID = $right.ID;
$Complex$955332131 = $right.Complex;
$Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
$Complex$2105709326.ID = $Complex$955332131.ID;
$Complex$2105709326.Name = $Complex$955332131.Name;
$left.Complex = $Complex$2105709326;
$left
}
}
Limpiar sería:
(left, right) =>
{
left.ID = right.ID;
var complexSource = right.Complex;
var complexDestination = new NestedDestinationType();
complexDestination.ID = complexSource.ID;
complexDestination.Name = complexSource.Name;
left.Complex = complexDestination;
return left;
}
Ese es el código que mapea las propiedades en estos tipos:
public class NestedSourceType
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ComplexSourceType
{
public int ID { get; set; }
public NestedSourceType Complex { get; set; }
}
public class NestedDestinationType
{
public int ID { get; set; }
public string Name { get; set; }
}
public class ComplexDestinationType
{
public int ID { get; set; }
public NestedDestinationType Complex { get; set; }
}
El código manual para hacer esto es:
var destination = new ComplexDestinationType
{
ID = source.ID,
Complex = new NestedDestinationType
{
ID = source.Complex.ID,
Name = source.Complex.Name
}
};
El problema es que cuando compilo LambdaExpression
y LambdaExpression
el delegate
resultante, es aproximadamente 10 veces más lento que la versión manual. No tengo idea de por qué es eso. Y la idea general sobre esto es el máximo rendimiento sin el tedio del mapeo manual.
Cuando tomo el código de Bart de Smet de su publicación de blog sobre este tema y comparo la versión manual del cálculo de números primos versus el árbol de expresiones compilado, son completamente idénticos en rendimiento.
¿Qué puede causar esta gran diferencia cuando la vista de depuración de LambdaExpression
parece a lo que cabría esperar?
EDITAR
Como solicité, agregué el punto de referencia que utilicé:
public static ComplexDestinationType Foo;
static void Benchmark()
{
var mapper = new DefaultMemberMapper();
var map = mapper.CreateMap(typeof(ComplexSourceType),
typeof(ComplexDestinationType)).FinalizeMap();
var source = new ComplexSourceType
{
ID = 5,
Complex = new NestedSourceType
{
ID = 10,
Name = "test"
}
};
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
Foo = new ComplexDestinationType
{
ID = source.ID + i,
Complex = new NestedDestinationType
{
ID = source.Complex.ID + i,
Name = source.Complex.Name
}
};
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
Foo = mapper.Map<ComplexSourceType, ComplexDestinationType>(source);
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
var func = (Func<ComplexSourceType, ComplexDestinationType, ComplexDestinationType>)
map.MappingFunction;
var destination = new ComplexDestinationType();
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
Foo = func(source, new ComplexDestinationType());
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
El segundo es comprensiblemente más lento que hacerlo manualmente ya que implica una búsqueda en el diccionario y algunas instancias de objetos, pero el tercero debe ser tan rápido como el delegado sin procesar que se está invocando y el lanzamiento de Delegate
a Func
ocurre fuera del lazo.
Traté de envolver el código manual en una función también, pero recuerdo que no hizo una diferencia notable. De cualquier forma, una llamada a función no debe agregar un orden de magnitud de sobrecarga.
También hago el punto de referencia dos veces para asegurarme de que el JIT no interfiera.
EDITAR
Puede obtener el código para este proyecto aquí:
https://github.com/JulianR/MemberMapper/
Usé la extensión del depurador Sons-of-Strike como se describe en la publicación de blog de Bart de Smet para volcar el IL generado del método dinámico:
IL_0000: ldarg.2
IL_0001: ldarg.1
IL_0002: callvirt 6000003 ComplexSourceType.get_ID()
IL_0007: callvirt 6000004 ComplexDestinationType.set_ID(Int32)
IL_000c: ldarg.1
IL_000d: callvirt 6000005 ComplexSourceType.get_Complex()
IL_0012: brfalse IL_0043
IL_0017: ldarg.1
IL_0018: callvirt 6000006 ComplexSourceType.get_Complex()
IL_001d: stloc.0
IL_001e: newobj 6000007 NestedDestinationType..ctor()
IL_0023: stloc.1
IL_0024: ldloc.1
IL_0025: ldloc.0
IL_0026: callvirt 6000008 NestedSourceType.get_ID()
IL_002b: callvirt 6000009 NestedDestinationType.set_ID(Int32)
IL_0030: ldloc.1
IL_0031: ldloc.0
IL_0032: callvirt 600000a NestedSourceType.get_Name()
IL_0037: callvirt 600000b NestedDestinationType.set_Name(System.String)
IL_003c: ldarg.2
IL_003d: ldloc.1
IL_003e: callvirt 600000c ComplexDestinationType.set_Complex(NestedDestinationType)
IL_0043: ldarg.2
IL_0044: ret
No soy un experto en IL, pero parece bastante directo y exactamente lo que esperarías, ¿no? Entonces, ¿por qué es tan lento? Sin extrañas operaciones de boxeo, sin instancias ocultas, nada. No es exactamente lo mismo que el árbol de expresiones anterior, ya que también hay un cheque null
a la right.Complex
. right.Complex
ahora.
Este es el código para la versión manual (obtenida a través de Reflector):
L_0000: ldarg.1
L_0001: ldarg.0
L_0002: callvirt instance int32 ComplexSourceType::get_ID()
L_0007: callvirt instance void ComplexDestinationType::set_ID(int32)
L_000c: ldarg.0
L_000d: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_0012: brfalse.s L_0040
L_0014: ldarg.0
L_0015: callvirt instance class NestedSourceType ComplexSourceType::get_Complex()
L_001a: stloc.0
L_001b: newobj instance void NestedDestinationType::.ctor()
L_0020: stloc.1
L_0021: ldloc.1
L_0022: ldloc.0
L_0023: callvirt instance int32 NestedSourceType::get_ID()
L_0028: callvirt instance void NestedDestinationType::set_ID(int32)
L_002d: ldloc.1
L_002e: ldloc.0
L_002f: callvirt instance string NestedSourceType::get_Name()
L_0034: callvirt instance void NestedDestinationType::set_Name(string)
L_0039: ldarg.1
L_003a: ldloc.1
L_003b: callvirt instance void ComplexDestinationType::set_Complex(class NestedDestinationType)
L_0040: ldarg.1
L_0041: ret
Parece idéntico a mí ...
EDITAR
Seguí el enlace en la respuesta de Michael B sobre este tema. ¡Intenté implementar el truco en la respuesta aceptada y funcionó! Si desea un resumen del truco: crea un ensamblaje dinámico y compila el árbol de expresiones en un método estático en ese ensamblaje y, por alguna razón, es 10 veces más rápido. Una desventaja de esto es que mis clases de referencia eran internas (en realidad, las clases públicas anidadas en una interna) y lanzó una excepción cuando traté de acceder a ellas porque no eran accesibles. No parece haber una solución alternativa, pero simplemente puedo detectar si los tipos a los que se hace referencia son internos o no y decidir qué método de compilación usar.
Sin embargo, lo que todavía me molesta es por qué ese método de números primos es idéntico en rendimiento al árbol de expresiones compilado.
Y de nuevo, doy la bienvenida a cualquiera que ejecute el código en ese repositorio de GitHub para confirmar mis mediciones y asegurarse de que no estoy loco :)
Creo que ese es el impacto de tener Reflexión en este punto. El segundo método es usar la reflexión para obtener y establecer los valores. Por lo que puedo ver, en este punto, no es el delegado, sino el reflejo que cuesta su tiempo.
Acerca de la tercera solución: También las expresiones Lambda deben evaluarse en tiempo de ejecución, lo que también le cuesta tiempo. Y eso no es pocos ...
Por lo tanto, nunca obtendrá la segunda y la tercera solución tan rápido como la copia manual.
Eche un vistazo a mis ejemplos de código aquí. Piensa que es la solución más rápida que puedes tomar, si no quieres la codificación manual: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
Esto es bastante extraño para un gran oído por casualidad. Hay algunas cosas para tener en cuenta. En primer lugar, el código compilado VS tiene diferentes propiedades aplicadas que pueden influir en la fluctuación de fase para optimizar de forma diferente.
¿Está incluyendo la primera ejecución para el delegado compilado en estos resultados? No deberías, debes ignorar la primera ejecución de cualquiera de las rutas de código. También debe convertir el código normal en un delegado ya que la invocación de delegados es un poco más lenta que la invocación de un método de instancia, que es más lento que invocar un método estático.
En cuanto a otros cambios, hay algo para explicar el hecho de que el delegado compilado tiene un objeto de cierre que no se está utilizando aquí, pero significa que se trata de un delegado específico que podría ser un poco más lento. Notarás que el delegado compilado tiene un objeto objetivo y todos los argumentos se desplazan hacia abajo en uno.
También los métodos generados por lcg se consideran estáticos, que tienden a ser más lentos cuando se compilan a delegados que los métodos de instancia debido a la conmutación de registros de negocios. (Duffy dijo que el puntero "this" tiene un registro reservado en CLR y cuando tienes un delegado para una estática, debe cambiarse a un registro diferente invocando una ligera sobrecarga). Finalmente, el código generado en tiempo de ejecución parece ejecutarse un poco más lento que el código generado por VS. El código generado en tiempo de ejecución parece tener un espacio aislado extra y se inicia desde un ensamblaje diferente (intente utilizar algo como ldftn opcode o calli opcode si no me cree, esos delegados de reflection compilados pero no le permitirán ejecutarlos realmente ) que invoca una sobrecarga mínima.
También estás corriendo en modo de lanzamiento ¿no? Hubo un tema similar donde revisamos este problema aquí: ¿Por qué Func <> creado desde Expression <Func <>> más lento que Func <> declarado directamente?
Editar: También vea mi respuesta aquí: DynamicMethod es mucho más lento que la función compilada de IL
Lo principal es que debe agregar el siguiente código al ensamblaje donde planea crear e invocar el código generado en tiempo de ejecución.
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]
Y para usar siempre un tipo de delegado incorporado o uno de un ensamblado con esos indicadores.
La razón es que el código dinámico anónimo se aloja en un ensamblado que siempre se marca como confianza parcial. Al permitir llamadas que sean parcialmente confiables, puede omitir parte del saludo. La transparencia significa que su código no aumentará el nivel de seguridad (es decir, un comportamiento lento), y, finalmente, el verdadero truco es invocar un tipo de delegado alojado en un ensamblaje marcado como verificación de omisión. Func<int,int>#Invoke
es completamente confiable, por lo que no se necesita verificación. Esto le dará el rendimiento del código generado a partir del compilador VS. Al no utilizar estos atributos, está viendo una sobrecarga en .NET 4. Puede pensar que SecurityRuleSet.Level1 sería una buena forma de evitar esta sobrecarga, pero cambiar los modelos de seguridad también es costoso.
En resumen, agregue esos atributos, y luego su prueba de rendimiento de micro-bucle, se ejecutará más o menos lo mismo.
Parece que te estás ejecutando por encima de la invocación. Independientemente de la fuente, sin embargo, si su método se ejecuta más rápido cuando se carga desde un ensamblado compilado, simplemente compilarlo en un ensamblaje y cargarlo. Ver mi respuesta en Why es Func <> creado desde Expression <Func <>> más lento que Func <> declarado directamente? para más detalles sobre cómo.
Puede compilar Expression Tree manualmente a través de Reflection.Emit
. En general, proporcionará un tiempo de compilación más rápido (en mi caso por debajo de ~ 30 veces más rápido), y le permitirá ajustar el rendimiento del resultado emitido. Y no es tan difícil hacerlo, especialmente si sus expresiones son subconjuntos conocidos limitados.
La idea es usar ExpressionVisitor
para recorrer la expresión y emitir el IL para el tipo de expresión correspondiente. También es "bastante" simple escribir su propio visitante para manejar el subconjunto conocido de expresiones, y recurrir a Expression.Compile
normal para los tipos de expresiones aún no compatibles .
En mi caso estoy generando el delegado:
Func<object[], object> createA = state =>
new A(
new B(),
(string)state[11],
new ID[2] { new D1(), new D2() }) {
Prop = new P(new B()), Bop = new B()
};
La prueba crea el árbol de expresiones correspondiente y compara su Expression.Compile
vs visiting y emitiendo el IL y luego creando un delegado desde DynamicMethod
.
Los resultados:
Compile Expression 3000 veces: 814
Invocar compilado Expresión 5000000 veces: 724
Emitir desde Expression 3000 veces: 36
Ejecutar expresión emitida 5000000 veces: 722
36 vs 814 al compilar manualmente.
Aquí el código completo .