我对C#中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));
}
答案 0 :(得分:211)
我读过动态会让编译器再次运行,但是它做了什么。它是否必须使用动态作为参数重新编译整个方法,或者更确切地说那些具有动态行为/上下文的行(?)
这是交易。
对于程序中动态类型的每个表达式,编译器会发出代码,生成表示操作的单个“动态调用站点对象”。所以,例如,如果你有:
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说“嗯,有人试图在这个对象上动态调用方法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;
然后有三个动态调用网站。一个用于动态调用Foo,一个用于动态添加,另一个用于从动态到int的动态转换。每个人都有自己的运行时分析和自己的分析结果缓存。
有意义吗?
答案 1 :(得分:94)
更新:添加了预编译和延迟编译的基准
更新2:原来,我错了。请参阅Eric Lippert的帖子,以获得完整正确的答案。我为了基准数字而离开这里
*更新3:根据Mark Gravell's answer to this question添加了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>));
}
从代码中可以看出,我尝试以七种不同的方式调用一个简单的无操作方法:
dynamic
Action
(从结果中排除编译时间)。Action
,使用非线程安全的Lazy变量(因此包括编译时间)每个在一个简单的循环中被称为100万次。以下是时间结果:
直接:3.4248ms
动态:45.0728ms
反思:888.4011ms
预编译:21.9166ms
懒人编译:30.2045ms
ILEmitted:8.4918ms
懒惰:14.3483ms
因此,虽然使用dynamic
关键字比直接调用方法要长一个数量级,但它仍然可以在大约50毫秒内完成操作一百万次,这使得它比反射快得多。如果我们调用的方法试图做一些密集的事情,比如将几个字符串组合在一起或者在集合中搜索值,那么这些操作可能远远超过直接调用和dynamic
调用之间的差异。
性能只是不必要地使用dynamic
的众多好理由之一,但是当您处理真正的dynamic
数据时,它可以提供远远超过缺点的优势。
根据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();
}),
......以下是基准测试结果:
因此,如果您可以预先确定需要调用的特定方法,则调用引用该方法的缓存委托与调用方法本身的速度一样快。但是,如果您需要确定在调用它时调用哪个方法,则为其创建委托非常昂贵。