功能与自定义委托的性能

see*_*per 6 c# performance jit

我正在研究一些对性能至关重要的代码,并且发现使用委托调用匿名方法比通过Func委托调用相同的代码更糟糕。

public class DelegateTests
{
    public delegate int GetValueDelegate(string test);

    private Func<string, int> getValueFunc;

    private GetValueDelegate getValueDelegate;

    public DelegateTests()
    {
        getValueDelegate = (s) => 42;
        getValueFunc = (s) => 42;                        
    }

    [Benchmark]
    public int CallWithDelegate()
    {
        return getValueDelegate.Invoke("TEST");
    }

    [Benchmark]
    public int CallWithFunc()
    {
        return getValueFunc.Invoke("TEST");
    }
}
Run Code Online (Sandbox Code Playgroud)

BenchmarkDotNet 给出:

// * Summary *

BenchmarkDotNet=v0.10.4, OS=Windows 10.0.14393
Processor=Intel Core i7-4770HQ CPU 2.20GHz (Haswell), ProcessorCount=2
Frequency=10000000 Hz, Resolution=100.0000 ns, Timer=UNKNOWN
  [Host]    : Clr 4.0.30319.42000, 32bit LegacyJIT-v4.6.1637.0
  RyuJitX64 : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0

Job=RyuJitX64  Jit=RyuJit  Platform=X64

           Method |      Mean |     Error |    StdDev |
----------------- |----------:|----------:|----------:|
 CallWithDelegate | 0.9926 ns | 0.0559 ns | 0.0783 ns |
     CallWithFunc | 0.8763 ns | 0.0168 ns | 0.0131 ns |

// * Hints *
Outliers
  DelegateTests.CallWithFunc: RyuJitX64 -&gt; 3 outliers were removed

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements

// ***** BenchmarkRunner: End *****
Run Code Online (Sandbox Code Playgroud)

如我们所见,使用Func委托调用函数比使用调用函数更快GetValueDelegate。我正在尝试寻找证据证明其为何如此运行。查看JIT优化的机器代码

    26:             return getValueDelegate.Invoke(&quot;TEST&quot;);
00E105C0 8B 49 08             mov         ecx,dword ptr [ecx+8]  
00E105C3 8B 15 C4 22 71 03    mov         edx,dword ptr ds:[37122C4h]  
00E105C9 8B 41 0C             mov         eax,dword ptr [ecx+0Ch]  
00E105CC 8B 49 04             mov         ecx,dword ptr [ecx+4]  
00E105CF FF D0                call        eax  
00E105D1 C3                   ret 
Run Code Online (Sandbox Code Playgroud)

    32:             return getValueFunc.Invoke(&quot;TEST&quot;);
00E10608 8B 49 04             mov         ecx,dword ptr [ecx+4]  
00E1060B 8B 15 C4 22 71 03    mov         edx,dword ptr ds:[37122C4h]  
00E10611 8B 41 0C             mov         eax,dword ptr [ecx+0Ch]  
00E10614 8B 49 04             mov         ecx,dword ptr [ecx+4]  
00E10617 FF D0                call        eax  
00E10619 C3                   ret 
Run Code Online (Sandbox Code Playgroud)

他们看起来很像。我开始认为这对于两个委托在Invoke方法内部可能有所不同。它们都从MulticastDelegate派生,这是CLR上所有委托的必要条件。为什么一个比另一个快?

更新

这是使用LegacyJitx86的数字。请注意,我只是对为什么有所不同感兴趣。顺便说一句,交换序列或可变顺序不会影响结果

// * Summary *

BenchmarkDotNet=v0.10.4, OS=Windows 10.0.14393
Processor=Intel Core i7-4770HQ CPU 2.20GHz (Haswell), ProcessorCount=2
Frequency=10000000 Hz, Resolution=100.0000 ns, Timer=UNKNOWN
  [Host]       : Clr 4.0.30319.42000, 32bit LegacyJIT-v4.6.1637.0
  LegacyJitX86 : Clr 4.0.30319.42000, 32bit LegacyJIT-v4.6.1637.0

Job=LegacyJitX86  Jit=LegacyJit  Platform=X86
Runtime=Clr

           Method |      Mean |     Error |    StdDev |
----------------- |----------:|----------:|----------:|
 CallWithDelegate | 2.3385 ns | 0.0361 ns | 0.0320 ns |
     CallWithFunc | 2.0144 ns | 0.0410 ns | 0.0384 ns |

// * Hints *
Outliers
  DelegateTests.CallWithDelegate: LegacyJitX86 -&gt; 1 outlier  was  removed

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements

// ***** BenchmarkRunner: End *****
Run Code Online (Sandbox Code Playgroud)

Joe*_*nta 4

使用我机器上所有内容的当前版本运行它们,我发现没有一致的赢家。

运行#1

在这次运行中,CallWithFunc平均时间加误差是CallWithDelegate平均时间减误差的 99%,所以我觉得可能还有一些旧行为的残余......


BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.101
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
  DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


Run Code Online (Sandbox Code Playgroud)
|           Method |     Mean |     Error |    StdDev |
|----------------- |---------:|----------:|----------:|
| CallWithDelegate | 1.103 ns | 0.0024 ns | 0.0019 ns |
|     CallWithFunc | 1.090 ns | 0.0050 ns | 0.0044 ns |
Run Code Online (Sandbox Code Playgroud)

运行#2

但当我再次运行它时,获胜者实际上翻转了,所以我的猜测是,如果有一个因素有利于其中一个或另一个,那么它可能是非常具体的东西。

例如,也许更快的回调恰好与其他一些重要的事情位于同一个缓存行上,而较慢的回调可能是唯一将回调保留在CPU缓存中的事情(在这种情况下,更改字段的顺序并标记类可能[StructLayout(LayoutKind.Sequential)]会揭示一些东西)。


BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.101
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
  DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT

Run Code Online (Sandbox Code Playgroud)
|           Method |     Mean |     Error |    StdDev |
|----------------- |---------:|----------:|----------:|
| CallWithDelegate | 1.062 ns | 0.0036 ns | 0.0030 ns |
|     CallWithFunc | 1.094 ns | 0.0039 ns | 0.0034 ns |
Run Code Online (Sandbox Code Playgroud)