Tengo una pregunta sobre el rendimiento de dynamic
en C #. He leído dynamic
que el compilador vuelve a funcionar, pero ¿qué hace?
¿Tiene que volver a compilar todo el método con la dynamic
variable utilizada como parámetro o solo aquellas líneas con comportamiento / contexto dinámico?
Me di cuenta de que el uso de dynamic
variables puede ralentizar un bucle simple en 2 órdenes de magnitud.
Código con el que he jugado:
internal class Sum2
{
public int intSum;
}
internal class Sum
{
public dynamic DynSum;
public int intSum;
}
class Program
{
private const int ITERATIONS = 1000000;
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
dynamic param = new Object();
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
Console.ReadKey();
}
private static void Sum(Stopwatch stopwatch)
{
var sum = 0;
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch, dynamic param)
{
var sum = new Sum2();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
}
private static void DynamicSum(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.DynSum += i;
}
stopwatch.Stop();
Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
c#
performance
dynamic
Lukasz Madon
fuente
fuente
Respuestas:
Aquí está el trato.
Para cada expresión en su programa que sea de tipo dinámico, el compilador emite un código que genera un único "objeto de sitio de llamada dinámica" que representa la operación. Entonces, por ejemplo, si tiene:
entonces el compilador generará código que es moralmente así. (El código real es bastante más complejo; esto se simplifica para fines de presentación).
¿Ves cómo funciona esto hasta ahora? Generamos el sitio de llamada una vez , no importa cuántas veces llame a M. El sitio de llamada vive para siempre después de que lo genera una vez. El sitio de la llamada es un objeto que representa "aquí va a haber una llamada dinámica a Foo".
Bien, ahora que tiene el sitio de la llamada, ¿cómo funciona la invocación?
El sitio de la llamada es parte del Dynamic Language Runtime. El DLR dice "hmm, alguien está intentando hacer una invocación dinámica de un método para este objeto aquí. ¿Sé algo sobre eso? No. Entonces será mejor que lo averigüe".
El DLR luego interroga al objeto en d1 para ver si es algo especial. Tal vez sea un objeto COM heredado, o un objeto Iron Python, o un objeto Iron Ruby, o un objeto IE DOM. Si no es ninguno de ellos, debe ser un objeto C # ordinario.
Este es el punto donde el compilador se inicia nuevamente. No hay necesidad de un analizador o analizador, por lo que el DLR inicia una versión especial del compilador de C # que solo tiene el analizador de metadatos, el analizador semántico para expresiones y un emisor que emite árboles de expresión en lugar de IL.
El analizador de metadatos usa Reflection para determinar el tipo de objeto en d1, y luego lo pasa al analizador semántico para preguntar qué sucede cuando se invoca dicho objeto en el método Foo. El analizador de resolución de sobrecarga se da cuenta de eso, y luego construye un árbol de expresión, como si hubiera llamado a Foo en un árbol de expresión lambda, que representa esa llamada.
El compilador de C # luego pasa el árbol de expresión al DLR junto con una política de caché. La política suele ser "la segunda vez que ve un objeto de este tipo, puede reutilizar este árbol de expresión en lugar de volver a llamarme". El DLR luego llama a Compilar en el árbol de expresión, que invoca el compilador de árbol de expresión a IL y escupe un bloque de IL generado dinámicamente en un delegado.
El DLR luego almacena en caché este delegado en un caché asociado con el objeto del sitio de la llamada.
Luego invoca al delegado y se produce la llamada de Foo.
La segunda vez que llama a M, ya tenemos un sitio de llamadas. El DLR interroga nuevamente al objeto, y si el objeto es del mismo tipo que la última vez, extrae al delegado del caché y lo invoca. Si el objeto es de un tipo diferente, la memoria caché falla y todo el proceso comienza de nuevo; Hacemos un análisis semántico de la llamada y almacenamos el resultado en el caché.
Esto sucede para cada expresión que implica dinámica. Entonces, por ejemplo, si tienes:
entonces hay tres sitios de llamadas dinámicas. Uno para la llamada dinámica a Foo, uno para la adición dinámica y otro para la conversión dinámica de dinámico a int. Cada uno tiene su propio análisis de tiempo de ejecución y su propio caché de resultados de análisis.
¿Tener sentido?
fuente
Actualización: Se agregaron puntos de referencia precompilados y compilados de manera diferida
Actualización 2: Resulta que estoy equivocado. Vea la publicación de Eric Lippert para obtener una respuesta completa y correcta. Dejo esto aquí por el bien de los números de referencia
* Actualización 3: Se agregaron puntos de referencia IL-Emitted y Lazy IL-Emitted, según la respuesta de Mark Gravell a esta pregunta .
Que yo sepa, el uso de ladynamic
palabra clave no causa ninguna compilación adicional en tiempo de ejecución en sí misma (aunque imagino que podría hacerlo en circunstancias específicas, dependiendo de qué tipo de objetos respaldan sus variables dinámicas).Con respecto al rendimiento,
dynamic
inherentemente introduce algo de sobrecarga, pero no tanto como podría pensar. Por ejemplo, acabo de ejecutar un punto de referencia que se ve así:Como puede ver en el código, trato de invocar un método simple sin operación de siete maneras diferentes:
dynamic
Action
precompilado en tiempo de ejecución (excluyendo así el tiempo de compilación de los resultados).Action
que se compila la primera vez que se necesita, usando una variable Lazy no segura para subprocesos (incluyendo el tiempo de compilación)Cada uno se llama 1 millón de veces en un bucle simple. Aquí están los resultados de tiempo:
Entonces, aunque usar la
dynamic
palabra clave toma un orden de magnitud más largo que llamar al método directamente, aún logra completar la operación un millón de veces en aproximadamente 50 milisegundos, por lo que es mucho más rápido que la reflexión. Si el método que llamamos intentara hacer algo intensivo, como combinar algunas cadenas o buscar un valor en una colección, esas operaciones probablemente superarían con creces la diferencia entre una llamada directa y unadynamic
llamada.El rendimiento es solo una de las muchas buenas razones para no usar
dynamic
innecesariamente, pero cuando se trata dedynamic
datos reales , puede proporcionar ventajas que superan con creces las desventajas.Actualización 4
Basado en el comentario de Johnbot, dividí el área de Reflection en cuatro pruebas separadas:
... y aquí están los resultados de referencia:
Entonces, si puede predeterminar un método específico que necesitará llamar mucho, invocar a un delegado en caché que se refiera a ese método es tan rápido como llamar al método en sí. Sin embargo, si necesita determinar a qué método llamar justo cuando está a punto de invocarlo, crear un delegado es muy costoso.
fuente
dynamic
supuesto con pérdida:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);