有一个dynamicvariables如何影响性能?
我有一个关于在C#中的dynamic性能的问题。 我读过dynamic让编译器再次运行,但是它做了什么?
是否必须重新编译整个方法,将dynamicvariables用作参数,还是仅使用具有dynamic行为/上下文的那些行?
我注意到,使用dynamicvariables可以减慢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)); }
我读过dynamic让编译器再次运行,但它做了什么。 它是否必须重新编译整个方法,将dynamic用作参数,还是使用dynamic行为/上下文(?)
这是交易。
对于程序中每个expression式都是dynamictypes的expression式 ,编译器会发出代码来生成表示操作的单个“dynamic调用站点对象”。 所以,举个例子,如果你有:
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);
看看这是如何工作到目前为止? 无论您打电话给多less次,我们都会生成一次呼叫站点。呼叫站点在您生成一次后永远存在。 呼叫站点是一个对象,表示“这里将会有一个dynamic的呼叫Foo”。
好的,现在你已经有了呼叫站点,调用是如何工作的?
呼叫站点是dynamic语言运行时的一部分。 DLR说:“嗯,有人试图在这个对象上dynamic地调用foo的方法,我知道这个吗?不,那么我最好找出来。
然后,DLR询问d1中的对象,看它是否有特殊之处。 也许它是一个传统的COM对象,或一个Iron Python对象,一个Iron Ruby对象或一个IE DOM对象。 如果它不是那些那么它必须是一个普通的C#对象。
这是编译器重新启动的地方。 DLR不需要词法分析器或分析器,因此DLR启动了一个特殊版本的C#编译器,它只包含元数据分析器,expression式的语义分析器以及发射expression式树而不是IL的发射器。
元数据分析器使用Reflection来确定d1中对象的types,然后将其传递给语义分析器,以询问在方法Foo上调用此类对象时会发生什么情况。 重载分辨率分析器会计算出来,然后构build一个expression式树 – 就像您在expression式树lambda中称为Foo – 表示该调用一样。
C#编译器随后将该expression式树与高速caching策略一起传递回DLR。 该策略通常是“第二次看到这种types的对象时,可以重新使用这个expression式树,而不是再次打电话给我”。 然后,DLR调用expression式树上的Compile,调用expression式树到IL编译器,并在代理中吐出dynamic生成的IL块。
然后,DLR将此代理caching到与呼叫站点对象关联的caching中。
然后它调用委托,并发生Foo调用。
你第二次打电话给M,我们已经有了一个呼叫站点。 DLR再次询问对象,如果对象与上次对象types相同,则将委托从caching中取出并调用它。 如果对象是不同的types,那么caching会丢失,整个过程会重新开始。 我们对调用进行语义分析并将结果存储在caching中。
这发生在涉及dynamic的每个expression式上。 所以例如,如果你有:
int x = d1.Foo() + d2;
那么有三个dynamic调用网站。 一个用于dynamic调用Foo,一个用于dynamic添加,另一个用于从dynamic到dynamic的int转换。 每个人都有自己的运行时分析和自己的分析结果caching。
合理?
更新:增加了预编译和惰性编译的基准
更新2:原来,我错了。 请参阅Eric Lippert的post以获取完整正确的答案。 为了基准数字,我在这里离开这里
*更新3:基于Mark Gravell对此问题的回答,添加了IL-Emitted和Lazy IL-Emitted基准。
据我所知, dynamic
关键字的使用不会在运行时导致任何额外的编译本身(尽pipe我认为它可以在特定情况下这样做,具体取决于支持dynamicvariables的对象types)。
关于performance, 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>)); }
从代码中可以看出,我试图用七种不同的方式调用一个简单的无操作方法:
- 直接方法调用
- 使用
dynamic
- 通过反思
- 使用在运行时预编译的
Action
(因此排除结果中的编译时间)。 - 使用第一次需要时编译的
Action
,使用非线程安全的Lazyvariables(因此包括编译时间) - 使用在testing之前创build的dynamic生成的方法。
- 使用dynamic生成的方法,在testing过程中被懒惰地实例化。
每个人在一个简单的循环中被称为100万次。 以下是时间结果:
直接:3.4248ms
dynamic:45.0728ms
反思:888.4011ms
预编译:21.9166ms
懒惰编译:30.2045ms
Ilemitted:8.4918ms
懒惰发表:14.3483ms
所以在使用dynamic
关键字比直接调用方法要长一个数量级的时候,它仍然设法在大约50毫秒内完成一百万次操作,使得它比reflection快得多。 如果我们所调用的方法试图做一些密集的工作,比如把几个string合并在一起或者在一个集合中search一个值,这些操作可能远远超过直接调用和dynamic
调用之间的差异。
性能只是不使用dynamic
不必要的许多好理由之一,但是当你处理真正的dynamic
数据时,它可以提供远远超过缺点的优点。
更新4
基于Johnbot的评论,我把Reflection区域分成四个单独的testing:
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(); }),
…这里是基准testing结果:
因此,如果您可以预先确定需要调用的特定方法,则调用引用该方法的caching委托与调用方法本身的速度差不多。 但是,如果您需要确定要调用的方法,则为其创build代理非常昂贵。