¿Cómo afecta tener una variable dinámica al rendimiento?


Tengo una pregunta sobre el rendimiento de dynamic en C#. He leído que dynamic hace que el compilador se ejecute de nuevo, pero ¿qué hace?

¿Tiene que recompilar todo el método con la variable dinámica utilizada como parámetro o solo aquellas líneas con comportamiento/contexto dinámico?

He notado que el uso de variables dinámicas puede ralentizar un simple bucle for 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));
    }
Author: numaroth, 2011-09-20

2 answers

He leído que dynamic hace que el compilador se ejecute de nuevo, pero lo que hace. ¿Tiene que recompilar todo el método con la dinámica utilizada como parámetro o más bien esas líneas con comportamiento dinámico/contexto(?)

Este es el trato.

Por cada expresión en su programa que es de tipo dinámico, el compilador emite código que genera un único "objeto dinámico de sitio de llamada" que representa la operación. Así, por ejemplo, si usted tiene:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

Entonces el el compilador generará código que es moralmente así. (El código real es un poco más complejo; esto se simplifica para fines de presentación.)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

¿Ves cómo funciona esto hasta ahora? Generamos el sitio de llamadas una vez, no importa cuántas veces llame a M. El sitio de llamadas vive para siempre después de que lo genere una vez. El sitio de la llamada es un objeto que representa "va a haber una llamada dinámica a Foo aquí".

OK, así que ahora que tienes el sitio de llamadas, ¿cómo ¿trabajo de invocación?

El sitio de la llamada es parte del Tiempo de ejecución del Lenguaje Dinámico. El DLR dice " hmm, alguien está tratando de hacer una invocación dinámica de un método foo en este objeto aquí. ¿Sé algo de eso? No. Entonces será mejor que lo averigüe."

El DLR entonces 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 cualquiera de esos entonces debe ser un ordinario Objeto C#.

Este es el punto donde el compilador se inicia de nuevo. No hay necesidad de un lexer o parser, 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 expresiones en lugar de IL.

El analizador de metadatos utiliza la reflexión 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 lo calcula, y luego construye un Árbol de Expresiones just como si hubieras llamado a Foo en un árbol de expresiones lambda that que representa esa llamada.

El compilador de C# luego pasa ese árbol de expresiones al DLR junto con una política de caché. La política suele ser "la segunda vez que vea un objeto de este tipo, puede reutilizar este árbol de expresiones en lugar de volver a llamarme". El DLR luego llama a Compile en el árbol de expresiones, que invoca el compilador de expresión-tree-to-IL y escupe un bloque de IL generado dinámicamente en un delegado.

El DLR almacena en caché este delegado en una caché asociada con el objeto call site.

Entonces invoca al delegado, y ocurre la llamada Foo.

La segunda vez que llamas a M, ya tenemos un sitio de llamadas. El DLR vuelve a interrogar al objeto, y si el objeto es del mismo tipo que la última vez, obtiene el delegado de la caché y lo invoca. Si el el objeto es de un tipo diferente, entonces la caché falla, y todo el proceso comienza de nuevo; hacemos análisis semántico de la llamada y almacenamos el resultado en la caché.

Esto sucede para cada expresión que involucra dinámica. Así, por ejemplo, si usted tiene:

int x = d1.Foo() + d2;

Luego hay tres sitios de llamadas dinámicas. Uno para la llamada dinámica a Foo, uno para la adición dinámica y uno para la conversión dinámica de dinámico a int. Cada uno tiene su propio tiempo de ejecución análisis y su propia caché de resultados de análisis.

¿Tiene sentido?

 190
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-20 16:53:19

Actualización: Se agregaron puntos de referencia precompilados y compilados perezosamente

Actualización 2: Resulta que estoy equivocado. Vea el post de Eric Lippert para 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 de emisión de IL y emisión de IL Perezosa, basados en La respuesta de Mark Gravell a esta pregunta.

Que yo sepa, el uso de la palabra clave dynamic no causa ninguna compilación adicional en tiempo de ejecución en y de sí mismo (aunque me imagino que podría hacerlo bajo circunstancias específicas, dependiendo de qué tipo de objetos respaldan sus variables dinámicas).

En cuanto al rendimiento, dynamic introduce inherentemente algunos gastos generales, pero no tanto como se podría pensar. Por ejemplo, acabo de ejecutar un benchmark que se ve así:

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

Como se puede ver en el código, trato de invocar un método simple no-op de siete maneras diferentes:

  1. método Directo call
  2. Usando dynamic
  3. Por reflexión
  4. Usando un Action precompilado en tiempo de ejecución (excluyendo así el tiempo de compilación de los resultados).
  5. Usando un Action que se compila la primera vez que se necesita, usando una variable perezosa no segura para subprocesos (por lo tanto, incluyendo el tiempo de compilación)
  6. Usando un método generado dinámicamente que se crea antes de la prueba.
  7. Utilizando un método generado dinámicamente que se instancian perezosamente durante el prueba.

Cada uno es llamado 1 millón de veces en un bucle simple. Aquí están los resultados del tiempo:

Directo: 3.4248 ms
Dinámica: 45.0728 ms
Reflexión: 888.4011 ms
Precompilado: 21.9166 ms
LazyCompiled: 30.2045 ms
Presentado: 8.4918 ms
LazyILEmitted: 14.3483 ms

Así que mientras que el uso de la palabra clave dynamic toma un orden de magnitud más largo que llamar al método directamente, todavía se las arregla para completar la operación a millones de veces en unos 50 milisegundos, por lo que es mucho más rápido que el reflejo. Si el método que llamamos estuviera tratando de hacer algo intensivo, como combinar unas pocas cadenas juntas o buscar un valor en una colección, esas operaciones probablemente superarían con creces la diferencia entre una llamada directa y una llamada dynamic.

El rendimiento es solo una de las muchas buenas razones para no usar dynamic innecesariamente, pero cuando se trata de datos verdaderamente dynamic, puede proporcionar ventajas que superan con creces la desventaja.

Actualización 4

Basado en el comentario de Johnbot, dividí el área de Reflexión en cuatro pruebas separadas: {[14]]}

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

... y aquí están los resultados de referencia:

introduzca la descripción de la imagen aquí

Así que si puedes predeterminar un método específico al que necesitarás llamar mucho, invocar 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 hacerlo invocarlo, crear un delegado para ello es muy caro.

 82
Author: StriplingWarrior,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:09