C# 中代表的意外表现不佳

Posted

技术标签:

【中文标题】C# 中代表的意外表现不佳【英文标题】:Unexpected poor performance of delegates in C# 【发布时间】:2012-05-23 23:09:41 【问题描述】:

我之前发过this question关于动态编译C#代码的帖子,答案引出了另一个问题。

一个建议是我使用委托,我尝试过并且效果很好。但是,它们的替补速度比直接调用慢了大约 8.4 倍,这是没有意义的。

这段代码有什么问题?

我的结果,.Net 4.0,64位,直接运行exe:62、514、530

public static int Execute(int i)  return i * 2; 

private void button30_Click(object sender, EventArgs e)

    CSharpCodeProvider foo = new CSharpCodeProvider();

    var res = foo.CompileAssemblyFromSource(
        new System.CodeDom.Compiler.CompilerParameters()
        
            GenerateInMemory = true,
            CompilerOptions = @"/optimize",                    
        ,
        @"public class FooClass  public static int Execute(int i)  return i * 2; "
    );

    var type = res.CompiledAssembly.GetType("FooClass");
    var obj = Activator.CreateInstance(type);
    var method = type.GetMethod("Execute");
    int i = 0, t1 = Environment.TickCount, t2;
    //var input = new object[]  2 ;

    //for (int j = 0; j < 10000000; j++)
    //
    //    input[0] = j;
    //    var output = method.Invoke(obj, input);
    //    i = (int)output;
    //

    //t2 = Environment.TickCount;

    //MessageBox.Show((t2 - t1).ToString() + Environment.NewLine + i.ToString());

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    
        i = Execute(j);
    

    t2 = Environment.TickCount;

    MessageBox.Show("Native: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    var func = (Func<int, int>) Delegate.CreateDelegate(typeof (Func<int, int>), method);

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    
        i = func(j);
    

    t2 = Environment.TickCount;

    MessageBox.Show("Dynamic delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    Func<int, int> funcL = Execute;

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    
        i = funcL(j);
    

    t2 = Environment.TickCount;

    MessageBox.Show("Delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

【问题讨论】:

一个疯狂的猜测是,在 Native 的情况下,编译器可以内联函数,而在 Delegate 的情况下它不能,并且必须执行方法调用。你能检查一下生成的低级代码吗? 问题不在于委托很慢,而在于常规方法调用非常快。当在这样的代码中使用时,它们应该花费 个周期,因为抖动优化器完全消除了调用并内联代码。您主要测量 for() 循环的成本。 反汇编显示调用。但是,即使它处于发布模式,我也不知道这是否 100% 可靠,因为我在 VS 中进行了该测试。 我有不同的结果:Native: 1513 Dynamic delegate: 655 Delegate: 1607 @IanC 它可能被 JIT 编译器内联。您应该检查调试器内的最终汇编代码(在运行测试后附加,否则 JIT 不会进行所有优化)。 【参考方案1】:

正如 Hans 在您的问题的 cmets 中提到的那样,Execute 方法非常简单,几乎可以肯定它会被您的“本机”测试中的抖动内联。

因此,您看到的不是标准方法调用和委托调用之间的比较,而是内联 i * 2 操作和委托调用之间的比较。 (而 i * 2 操作可能归结为一条机器指令,几乎是你能得到的最快速度。)

使您的Execute 方法更复杂一些以防止内联(和/或使用MethodImplOptions.NoInlining 编译器提示来实现);那么您将在标准方法调用和委托调用之间获得更真实的比较。在大多数情况下,差异可能可以忽略不计:

[MethodImpl(MethodImplOptions.NoInlining)]
static int Execute(int i)  return ((i / 63.53) == 34.23) ? -1 : (i * 2); 
public static volatile int Result;

private static void Main(string[] args)

    const int iterations = 100000000;

    
        Result = Execute(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        
            Result = Execute(i);
        
        s.Stop();
        Console.WriteLine("Native: " + s.ElapsedMilliseconds);
    

    
        Func<int, int> func;
        using (var cscp = new CSharpCodeProvider())
        
            var cp = new CompilerParameters  GenerateInMemory = true, CompilerOptions = @"/optimize" ;
            string src = @"public static class Foo  public static int Execute(int i)  return ((i / 63.53) == 34.23) ? -1 : (i * 2);  ";

            var cr = cscp.CompileAssemblyFromSource(cp, src);
            var mi = cr.CompiledAssembly.GetType("Foo").GetMethod("Execute");
            func = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), mi);
        

        Result = func(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        
            Result = func(i);
        
        s.Stop();
        Console.WriteLine("Dynamic delegate: " + s.ElapsedMilliseconds);
    

    
        Func<int, int> func = Execute;
        Result = func(42);  // pre-jit

        var s = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        
            Result = func(i);
        
        s.Stop();
        Console.WriteLine("Delegate: " + s.ElapsedMilliseconds);
    

【讨论】:

我添加了非内联提示并重新运行了测试,你就明白了。代表只慢了大约 50%。然而,奇怪的是,对动态编译方法的调用始终比调用本机方法快 91% 的时间。我想知道为什么会这样。【参考方案2】:

这是有道理的。委托不是函数指针。它们意味着类型检查、安全性和许多其他东西。它们更接近虚函数调用的速度(参见this post),即使性能影响来自完全不同的东西。

如需对不同调用技术(其中一些未在问题中提及)进行很好的比较,请阅读this article。

【讨论】:

同意。好像也正常。在发布模式下运行时,代表与本地调用相比,我的速度要慢 4 倍。 哇,我读到它们的速度应该只有一半左右。我可以指出认为这一点的专家。但是...人们无法争辩结果。 @DarinDimitrov 你运行了我的代码,但速度慢了 4 倍?我想知道为什么我会得到 8,那么。 @IanC 我想真正的比较是不可能的。使用 JIT 编译,每台机器的结果都会有所不同(你可以说它们非常慢)。此外,测试环境也太重要了(我读过一篇关于 Java 性能测试的好文章,但我不记得在哪里)。

以上是关于C# 中代表的意外表现不佳的主要内容,如果未能解决你的问题,请参考以下文章

sklearn 中 GMM 的意外性能不佳

按日期选择表现最好的销售代表

C#中的#代表啥?

C#变量定义中含问号代表啥

MVC模式中M,V,C每个代表意义,并简述在Struts中MVC的表现方式。

c# 中使用的“#if”指令是啥,“符号”代表啥?