动态变量如何影响性能?

Luk*_*don 119 c# performance dynamic

我对dynamicC#的性能有疑问.我读过dynamic让编译器再次运行,但是它做了什么?

是否必须dynamic使用用作参数的变量或仅具有动态行为/上下文的行重新编译整个方法?

我注意到使用dynamic变量可以将简单的for循环减慢2个数量级.

我玩过的代码:

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));
    }
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 221

我读过动态会让编译器再次运行,但是它做了什么.是否必须使用动态作为参数重新编译整个方法,或者更确切地说那些具有动态行为/上下文的行(?)

这是交易.

对于程序中具有动态类型的每个表达式,编译器会发出代码,生成表示操作的单个"动态调用站点对象".所以,例如,如果你有:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();
Run Code Online (Sandbox Code Playgroud)

然后编译器将生成像这样的道德规范的代码.(实际代码相当复杂;为了演示目的,这是简化的.)

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

看看到目前为止如何运作?无论您多少次呼叫M,我们都会生成一次呼叫站点.呼叫站点在您生成一次后永远存在.呼叫站点是一个对象,表示"这里将动态调用Foo".

好了,现在您已经有了呼叫站点,调用如何工作?

呼叫站点是动态语言运行时的一部分.DLR说"嗯,有人试图在这个对象上动态调用方法foo.我对此有所了解吗?不.那我最好找出来."

然后DLR询问d1中的对象以查看它是否特殊.也许它是遗留的COM对象,或Iron Python对象,或Iron Ruby对象,或IE DOM对象.如果它不是那些,那么它必须是普通的C#对象.

这是编译器再次启动的点.不需要词法分析器或解析器,因此DLR启动了一个特殊版本的C#编译器,它只有元数据分析器,表达式的语义分析器和发出表达式树而不是IL的发射器.

元数据分析器使用Reflection来确定d1中对象的类型,然后将其传递给语义分析器,以询问在方法Foo上调用此类对象时会发生什么.重载分辨率分析器计算出来,然后构建一个表达式树 - 就像你在表达式lambda中调用Foo一样 - 表示该调用.

然后,C#编译器将该表达式树与缓存策略一起传递回DLR.该策略通常是"第二次看到此类对象时,您可以重新使用此表达式树而不是再次回叫".然后,DLR在表达式树上调用Compile,它会调用表达式树到IL的编译器,并在委托中吐出一块动态生成的IL.

然后,DLR将此委托缓存在与调用站点对象关联的缓存中.

然后它调用委托,并发生Foo调用.

你第二次打电话给M,我们已经有了一个电话网站.DLR再次询问对象,如果对象与上次对象类型相同,则会将委托从缓存中取出并调用它.如果对象属于不同类型,则缓存未命中,整个过程重新开始; 我们对调用进行语义分析并将结果存储在缓存中.

每个涉及动态的表达都会发生这种情况.例如,如果你有:

int x = d1.Foo() + d2;
Run Code Online (Sandbox Code Playgroud)

然后有三个动态调用站点.一个用于动态调用Foo,一个用于动态添加,另一个用于从动态到int的动态转换.每个人都有自己的运行时分析和自己的分析结果缓存.

合理?

  • @Eric,"我们从csc.exe编译器中提取了C++代码的相关部分,并逐行将它们翻译成C#"然后人们认为Roslyn可能值得追求:) (8认同)
  • @ShuggyCoUk:有一个编译器即服务的想法已经开始了一段时间,但实际上需要运行时服务做代码分析是该项目的一个重要推动力,是的. (5认同)
  • @Roman:没有.csc.exe是用C++编写的,我们需要从C#轻松调用的东西.此外,主线编译器有自己的类型对象,但我们需要能够使用Reflection类型对象.我们从csc.exe编译器中提取了C++代码的相关部分,并将它们逐行转换为C#,然后构建一个库,供DLR调用. (4认同)

Str*_*ior 101

更新:添加了预编译和惰性编译的基准测试

更新2:结果,我错了.请参阅Eric Lippert的帖子,获得完整正确的答案.我为了基准数而离开这里

*更新3:根据Mark Gravell对此问题的回答,添加了IL-Emitted和Lazy IL-Emitted基准.

据我所知,使用dynamic关键字不会在运行时本身产生任何额外的编译(虽然我想它可以在特定情况下这样做,具体取决于支持动态变量的对象类型).

关于性能,dynamic本质上会引入一些开销,但不会像你想象的那么多.例如,我刚刚运行了一个如下所示的基准测试:

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>));
}
Run Code Online (Sandbox Code Playgroud)

从代码中可以看出,我尝试以七种不同的方式调用一个简单的无操作方法:

  1. 直接方法调用
  2. 运用 dynamic
  3. 通过反思
  4. 使用Action在运行时预编译的(因此从结果中排除编译时间).
  5. 使用一个Action在第一次需要时编译,使用非线程安全的Lazy变量(因此包括编译时间)
  6. 使用在测试之前创建的动态生成的方法.
  7. 使用动态生成的方法在测试期间进行延迟实例化.

每个在一个简单的循环中被称为100万次.以下是时间结果:

直接:3.4248ms
动态:45.0728ms
反射:888.4011ms
预编译:21.9166ms
LazyCompiled:30.2045ms
ILEmitted:8.4918ms
LazyILEmitted:14.3483ms

因此,虽然使用dynamic关键字比直接调用方法要长一个数量级,但它仍然设法在大约50毫秒内完成操作一百万次,这使得它比反射快得多.如果我们调用的方法试图做一些密集的事情,比如将几个字符串组合在一起或者在集合中搜索值,那么这些操作可能远远超过直接调用和dynamic调用之间的差异.

性能只是dynamic不必要地使用的众多好理由之一,但是当您处理真正的dynamic数据时,它可以提供远远超过缺点的优势.

更新4

根据Johnbot的评论,我将Reflection区域分解为四个单独的测试:

    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();
    }),
Run Code Online (Sandbox Code Playgroud)

......以下是基准测试结果:

在此输入图像描述

因此,如果您可以预先确定需要调用的特定方法,则调用引用该方法的缓存委托与调用方法本身的速度一样快.但是,如果您需要确定在调用它时调用哪个方法,则为其创建委托非常昂贵.

  • 您的性能数字肯定会显示DLR积极的缓存策略是如何得到回报的.如果你的例子做了一些愚蠢的事情,例如你每次打电话时都有不同的接收类型,那么当它无法利用其先前编译的分析结果的缓存时,你会发现动态版本非常慢.但是当它*可以*利用这一点时,圣洁的善良就是它的速度. (6认同)
  • 好吧,动态代码启动编译器的元数据导入器,语义分析器和表达式树发射器,然后在其输出上运行表达式树到编译器,所以我认为它开始是公平的在运行时编译器.仅仅因为它没有运行词法分析器并且解析器几乎不相关. (4认同)
  • 如此详细的回复,谢谢!我也想知道实际的数字. (2认同)