一尘不染

具有动态变量如何影响性能?

c#

我对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));
    }

阅读 349

收藏
2020-05-19

共1个答案

一尘不染

我读过动态使编译器再次运行,但它能做什么。它是否必须使用动态参数作为参数重新编译整个方法,还是使用动态行为/上下文(?)

这是交易。

对于程序中动态类型的每个 表达式 ,编译器都会发出代码,该代码生成代表操作的单个“动态调用站点对象”。因此,例如,如果您有:

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

那么编译器将生成道德上像这样的代码。(实际代码要复杂得多;出于演示目的,已对其进行了简化。)

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

到目前为止,如何运作?无论您呼叫M多少次,我们都会生成 一次 呼叫站点。 一旦 生成 一次
,呼叫站点将永久存在。呼叫站点是一个对象,表示“这里将是对Foo的动态呼叫”。

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

呼叫站点是动态语言运行时的一部分。DLR说:“嗯,有人试图对此here对象进行方法foo的动态调用。对此我是否一无所知?不,那么我最好找出来。”

然后DLR询问d1中的对象以查看它是否特殊。可能是旧版COM对象,Iron Python对象,Iron Ruby对象或IE
DOM对象。如果不是这些,则它必须是普通的C#对象。

这是编译器再次启动的点。不需要词法分析器或解析器,因此DLR启动了C#编译器的特殊版本,该版本仅具有元数据分析器,表达式的语义分析器以及发出表达式树而不是IL的发射器。

元数据分析器使用反射来确定d1中对象的类型,然后将其传递给语义分析器以询问在方法Foo上调用该对象时会发生什么。重载解析器会找出这个问题,然后构建一个表达式树-
就像您在表达式树lambda中调用Foo一样,代表该调用。

然后,C#编译器将该表达式树与缓存策略一起传递回DLR。该策略通常是“第二次看到这种类型的对象时,您可以重新使用此表达式树,而无需再次给我回电”。然后DLR在表达式树上调用Compile,后者会调用表达式树到IL的编译器,并在委托中吐出一块动态生成的IL。

然后,DLR将该委托缓存在与呼叫站点对象关联的缓存中。

然后,它调用委托,然后进行Foo调用。

第二次您致电M,我们已经有一个呼叫站点。DLR再次询问该对象,如果该对象与上次的类型相同,则它将委托从缓存中取出并调用它。如果对象的类型不同,则高速缓存将丢失,并且整个过程将重新开始;否则,整个过程将重新开始。我们对调用进行语义分析,并将结果存储在缓存中。

对于涉及动态的 每个表达式都会 发生这种情况。因此,例如,如果您有:

int x = d1.Foo() + d2;

那么就有 三个
动态呼叫站点。一种用于对Foo的动态调用,一种用于动态加法,一种用于从dynamic到int的动态转换。每个人都有自己的运行时分析和分析结果缓存。

合理?

2020-05-19