我对dynamicC#的性能有疑问。我读过dynamic使编译器再次运行,但是它做什么呢?
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)); }
我读过动态使编译器再次运行,但它能做什么。它是否必须使用动态参数作为参数重新编译整个方法,还是使用动态行为/上下文(?)
这是交易。
对于程序中动态类型的每个 表达式 ,编译器都会发出代码,该代码生成代表操作的单个“动态调用站点对象”。因此,例如,如果您有:
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的动态转换。每个人都有自己的运行时分析和分析结果缓存。
合理?