动态变量如何影响性能?

我有一个关于 dynamic在 C # 中的性能的问题。我读过 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));
}
60803 次浏览

更新: 添加了预编译和延迟编译基准测试

更新2: 事实证明,我错了。请参阅埃里克 · 利伯特的帖子以获得完整和正确的答案。我把它留在这里是为了基准数字

* 更新3: 基于 Mark Gravell 对这个问题的回答增加了发放和延迟发放白细胞介素的基准。

据我所知,使用 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>));
}

从代码中可以看出,我尝试用七种不同的方法调用一个简单的 no-op 方法:

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

每个命令在一个简单的循环中被调用100万次,下面是计时结果:

直接: 3.4248毫秒
动态: 45.0728毫秒
反射: 888.4011 ms
预编译: 21.9166 ms
LazyCompiled: 30.2045 ms
ILEmitt: 8.4918 ms
LazyILEmit: 14.3483 ms

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

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

更新4

基于 Johnbot 的评论,我将反射区域分为四个独立的测试:

    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();
}),

这是基准测试结果:

enter image description here

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

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

事情是这样的。

对于程序中每个动态类型的 表情,编译器都会发出代码,生成一个“动态调用站点对象”来表示操作。所以,举个例子,如果你有:

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 进行动态调用”。

好的,现在您已经得到了调用站点,那么调用是如何工作的呢?

呼叫网站是 Dynamic Language Runtime 的一部分。DLR 说: “嗯,有人试图在这里的这个对象上动态调用 foo 方法。我知道什么吗?没有。那我最好弄清楚”

然后 DLR 询问 d1中的对象,看看它是否有什么特殊之处。它可能是一个遗留 COM 对象,或者是一个 Iron Python 对象,或者是一个 Iron Ruby 对象,或者是一个 IE DOM 对象。如果它不是任何一个,那么它必须是一个普通的 C # 对象。

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

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

然后,C # 编译器将该表达式树以及缓存策略传递回 DLR。策略通常是“当您第二次看到这种类型的对象时,您可以重用这个表达式树,而不必再次调用我”。然后,DLR 在表达式树上调用 Compile,该表达式树调用 expression-tree-to-IL 编译器,并在委托中生成一个动态生成的 IL 块。

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

然后它调用委托,Foo 调用就发生了。

你第二次给 M 打电话,我们已经有一个通话网站了。DLR 再次询问对象,如果对象的类型与上次相同,它将从缓存中获取委托并调用它。如果对象的类型不同,那么缓存将丢失,整个过程将重新启动; 我们对调用进行语义分析,并将结果存储在缓存中。

这种情况发生在涉及动态的 每个表情上,例如:

int x = d1.Foo() + d2;

然后有 动态呼叫站点。一个用于对 Foo 的动态调用,一个用于动态添加,还有一个用于从 Dynamic 到 int 的动态转换。每一个都有自己的运行时分析和自己的分析结果缓存。

有道理吗?