返回值或通过Action <t>参数</t>返回之间的奇怪性能差异

时间:2010-09-16 15:12:04

标签: c# .net performance delegates

我很想知道从方法返回值或通过Action参数返回值之间的性能差异。

这有一个相关的问题  Performance of calling delegates vs methods

但是对于我的生活,我无法解释为什么返回一个值比调用一个委托返回值慢约30%。是.net Jitter(不是编译器..)内置我的简单委托(我不认为它那样做了)?

class Program
{
    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        A aa = new A();

        long l = 0;
        for( int i = 0; i < 100000000; i++ )
        {
            aa.DoSomething( i - 1, i, r => l += r );
        }

        sw.Stop();
        Trace.WriteLine( sw.ElapsedMilliseconds + " : " + l );

        sw.Reset();
        sw.Start();

        l = 0;
        for( int i = 0; i < 100000000; i++ )
        {
            l += aa.DoSomething2( i - 1, i );
        }

        sw.Stop();
        Trace.WriteLine( sw.ElapsedMilliseconds + " : " + l );
    }
}
class A
{
    private B bb = new B();

    public void DoSomething( int a, int b, Action<long> result )
    {
        bb.Add( a,b, result );
    }

    public long DoSomething2( int a, int b  )
    {
        return bb.Add2( a,b );
    }

}
class B
{
    public void Add( int a, int b, Action<long> result )
    {
        result( a + b );
    }

    public long Add2( int i, int i1 )
    {
        return i + i1;
    }
}

4 个答案:

答案 0 :(得分:2)

我对您的代码进行了一些更改。

  • 在定时部分之前移动了new A()
  • 在定时部分之前添加了预热代码以获取JIT的方法。
  • 在定时部分和循环之前创建Action<long>引用,这样就不必在每次迭代时创建它。这个似乎对执行时间有很大的影响。

以上是我做出上述更改后的结果。 vshost列指示代码是否在vshost.exe进程内执行(通过直接从Visual Studio运行)。我使用的是Visual Studio 2008,目标是.NET 3.5 SP1。

vshost?   Debug   Release
-------------------------
 YES       6405     3827
          11059     3092

 NO        4214     1691
           4607      811

请注意,根据构建配置执行环境,您会得到不同的结果。如果没别的话,结果很有意思。如果我有时间,我可以编辑我的答案以提供理论。

答案 1 :(得分:1)

奇怪的是,在VS中运行Release版本时,我没有看到您所描述的行为。我在运行Debug构建时看到它。我唯一能想到的是,在运行Debug构建时,基于返回的方法会增加额外开销,尽管我不够聪明,不知道为什么。

以下是其他有趣的内容:当我切换到x64版本(发布调试)时,这种差异就会消失。

如果我冒险猜测完全未经证实),可能是将64位long作为返回值传递的成本B.Add2A.DoSomething2中的Action<long>超过了在32位环境中传递Action<long>的重量。在64位环境中,这种节省将消失,因为long也需要64位。在任一配置的发布版本中,传递B.Add2的成本可能会消失,因为A.DoSomething2和{{1}}似乎都是内联的主要候选者。

有些人比我更了解这一点:我可以完全驳斥我刚才所说的一切。毕竟,我们都在这里学习;)

答案 2 :(得分:1)

对于初学者来说,您对new A()的呼叫正在按照您当前设置代码的方式进行计时。您需要确保在发布模式下运行并进行优化。此外,您还需要考虑JIT - 填写所有代码路径,这样您就可以保证在计算之前编译它们(除非您关注启动时间)。

当你尝试对大量原始操作(简单添加)进行计时时,我发现了一个问题。在这种情况下,您无法做出明确的结论,因为任何开销都将完全控制您的测量。

编辑: 在VS2008中针对.NET 3.5的发布模式中,我得到:

1719 : 9999999800000000
1337 : 9999999800000000

这似乎与许多其他答案一致。使用ILDasm为B.Add提供以下IL:

  IL_0000:  ldarg.3
  IL_0001:  ldarg.1
  IL_0002:  ldarg.2
  IL_0003:  add
  IL_0004:  conv.i8
  IL_0005:  callvirt   instance void class [mscorlib]System.Action`1<int64>::Invoke(!0)
  IL_000a:  ret

B.Add2是:

  IL_0000:  ldarg.1
  IL_0001:  ldarg.2
  IL_0002:  add
  IL_0003:  conv.i8
  IL_0004:  ret

所以看起来好像只是计算loadcallvirt

答案 3 :(得分:-1)

为什么不使用reflector来查找?