C# 动态变量如何影响性能?

C# 动态变量如何影响性能?,c#,performance,dynamic,C#,Performance,Dynamic,我对C#中的动态的性能有一个问题。我读过dynamic使编译器再次运行,但是它做什么呢 它是否必须使用动态变量作为参数重新编译整个方法,还是仅使用具有动态行为/上下文的行 我注意到使用动态变量可以将简单for循环的速度降低2个数量级 我玩过的代码: internal class Sum2 { public int intSum; } internal class Sum { public dynamic DynSum; public int intSum; } cla

我对C#中的
动态
的性能有一个问题。我读过
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));
    }
内部类Sum2
{
公共整数;
}
内部类和
{
公共动态动力;
公共整数;
}
班级计划
{
私有常量int迭代次数=1000000;
静态void Main(字符串[]参数)
{
var stopwatch=新秒表();
动态参数=新对象();
DynamicSum(秒表);
SumInt(秒表);
SumInt(秒表、参数);
总数(秒表);
DynamicSum(秒表);
SumInt(秒表);
SumInt(秒表、参数);
总数(秒表);
Console.ReadKey();
}
专用静态无效和(秒表秒表)
{
var总和=0;
秒表复位();
秒表。开始();
对于(int i=0;i
更新:添加了预编译和延迟编译的基准测试

更新2:事实证明,我错了。请参阅Eric Lippert的帖子以获得完整和正确的答案。为了基准数据,我将此留在这里

*更新3:添加了IL发射和延迟IL发射基准,基于

据我所知,使用
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使编译器再次运行,但它所做的是什么。它必须用dynamic作为参数重新编译整个方法,还是用dynamic行为/上下文(?)重新编译那些行

事情是这样的

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

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();
int x = d1.Foo() + d2;
然后,编译器将生成这样的代码(实际代码要复杂一点;为了表示的目的,这是简化的)

看到了吗?我们只生成一次调用站点,不管您调用M多少次。调用站点在生成一次后将永远存在。调用站点是一个表示“这里将有一个对Foo的动态调用”的对象

好的,现在您已经有了调用站点,调用是如何工作的

调用站点是动态语言运行库的一部分。DLR说:“嗯,有人试图在这个对象上动态调用一个方法foo。我知道这方面的情况吗?不知道。那我最好弄清楚。”

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

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

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

然后,C#编译器将该表达式树连同缓存策略一起传递回DLR
class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);
int x = d1.Foo() + d2;