为什么编译的委托比声明的委托更快?

时间:2018-05-03 07:53:26

标签: c# .net performance delegates linq-expressions

首先,这与Why is Func<> created from Expression> slower than Func<> declared directly?不同,令人惊讶的恰恰相反。另外,我在研究这个问题时发现的所有链接和问题都源自2010 - 2012年的时间段,所以我决定在这里开一个新问题,看看是否有关于当前代表状态的讨论.NET生态系统中的行为。

也就是说,我正在使用.NET Core 2.0和.NET 4.7.1,并且我看到了一些奇怪的性能指标,这些指标是根据编译表达式创建的委托与被描述和声明为CLR对象的委托创建的。 / p>

关于我如何偶然发现这个问题的一些背景,我正在做一个测试,涉及1,000和10,000个对象的数组中的数据选择,并注意到如果我使用编译表达式,它会获得更快的结果。我设法把它归结为一个非常简单的项目,可以重现这个问题,你可以在这里找到:

https://github.com/Mike-EEE/StackOverflow.Performance.Delegates

对于测试,我使用了两组基准测试,其特点是已编译的委托与声明的委托配对,从而产生四个总核心基准。

第一个委托集由一个返回空字符串的空委托组成。第二个集合是一个委托,其中包含一个简单的表达式。我想证明这个问题出现在最简单的委托以及具有已定义主体的委托中。

然后,这些测试通过优秀的Benchmark.NET性能产品在CLR运行时和.NET Core运行时上运行,从而产生8个总基准。此外,我还使用了同样出色的Benchmark.NET disassembly diagnoser来发出在基准测量的JIT期间遇到的反汇编。我将分享以下结果。

以下是运行基准测试的代码。你可以看到它非常简单:

[CoreJob, ClrJob, DisassemblyDiagnoser(true, printSource: true)]
public class Delegates
{
    readonly DelegatePair<string, string> _empty;
    readonly DelegatePair<string, int>    _expression;
    readonly string                       _message;

    public Delegates() : this(new DelegatePair<string, string>(_ => default, _ => default),
                              new DelegatePair<string, int>(x => x.Length, x => x.Length)) {}

    public Delegates(DelegatePair<string, string> empty, DelegatePair<string, int> expression,
                     string message = "Hello World!")
    {
        _empty      = empty;
        _expression = expression;
        _message    = message;
        EmptyDeclared();
        EmptyCompiled();
        ExpressionDeclared();
        ExpressionCompiled();
    }

    [Benchmark]
    public void EmptyDeclared() => _empty.Declared(default);

    [Benchmark]
    public void EmptyCompiled() => _empty.Compiled(default);

    [Benchmark]
    public void ExpressionDeclared() => _expression.Declared(_message);

    [Benchmark]
    public void ExpressionCompiled() => _expression.Compiled(_message);
}

这些是我在Benchmark.NET中看到的结果:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-4820K CPU 3.70GHz (Haswell), 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=2.1.300-preview2-008533
  [Host] : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0
  Core   : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT


             Method |  Job | Runtime |      Mean |     Error |    StdDev |
------------------- |----- |-------- |----------:|----------:|----------:|
      EmptyDeclared |  Clr |     Clr | 1.3691 ns | 0.0302 ns | 0.0282 ns |
      EmptyCompiled |  Clr |     Clr | 1.1851 ns | 0.0381 ns | 0.0357 ns |
 ExpressionDeclared |  Clr |     Clr | 1.3805 ns | 0.0314 ns | 0.0294 ns |
 ExpressionCompiled |  Clr |     Clr | 1.1431 ns | 0.0396 ns | 0.0371 ns |
      EmptyDeclared | Core |    Core | 1.5733 ns | 0.0329 ns | 0.0308 ns |
      EmptyCompiled | Core |    Core | 0.9326 ns | 0.0275 ns | 0.0244 ns |
 ExpressionDeclared | Core |    Core | 1.6040 ns | 0.0394 ns | 0.0368 ns |
 ExpressionCompiled | Core |    Core | 0.9380 ns | 0.0485 ns | 0.0631 ns |

请注意,使用已编译委托的基准测试始终更快。

最后,以下是每个基准测试所遇到的反汇编结果:

&#13;
&#13;
<style type="text/css">
	table { border-collapse: collapse; display: block; width: 100%; overflow: auto; }
	td, th { padding: 6px 13px; border: 1px solid #ddd; }
	tr { background-color: #fff; border-top: 1px solid #ccc; }
	tr:nth-child(even) { background: #f8f8f8; }
</style>
</head>
<body>
<table>
<thead>
<tr><th colspan="2">Delegates.EmptyDeclared</th></tr>
<tr>
<th>.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0</th>
<th>.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align:top;"><pre><code>
00007ffd`4f8f0ea0 StackOverflow.Performance.Delegates.Delegates.EmptyDeclared()
		public void EmptyDeclared() => _empty.Declared(default);
                                 ^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8f0ea4 4883c110        add     rcx,10h
00007ffd`4f8f0ea8 488b01          mov     rax,qword ptr [rcx]
00007ffd`4f8f0eab 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`4f8f0eaf 33d2            xor     edx,edx
00007ffd`4f8f0eb1 ff5018          call    qword ptr [rax+18h]
00007ffd`4f8f0eb4 90              nop

</code></pre></td>
<td style="vertical-align:top;"><pre><code>
00007ffd`39c8d8b0 StackOverflow.Performance.Delegates.Delegates.EmptyDeclared()
		public void EmptyDeclared() => _empty.Declared(default);
                                 ^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c8d8b4 4883c110        add     rcx,10h
00007ffd`39c8d8b8 488b01          mov     rax,qword ptr [rcx]
00007ffd`39c8d8bb 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`39c8d8bf 33d2            xor     edx,edx
00007ffd`39c8d8c1 ff5018          call    qword ptr [rax+18h]
00007ffd`39c8d8c4 90              nop

</code></pre></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr><th colspan="2">Delegates.EmptyCompiled</th></tr>
<tr>
<th>.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0</th>
<th>.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align:top;"><pre><code>
00007ffd`4f8e0ef0 StackOverflow.Performance.Delegates.Delegates.EmptyCompiled()
		public void EmptyCompiled() => _empty.Compiled(default);
                                 ^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8e0ef4 4883c110        add     rcx,10h
00007ffd`4f8e0ef8 488b4108        mov     rax,qword ptr [rcx+8]
00007ffd`4f8e0efc 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`4f8e0f00 33d2            xor     edx,edx
00007ffd`4f8e0f02 ff5018          call    qword ptr [rax+18h]
00007ffd`4f8e0f05 90              nop

</code></pre></td>
<td style="vertical-align:top;"><pre><code>
00007ffd`39c8d900 StackOverflow.Performance.Delegates.Delegates.EmptyCompiled()
		public void EmptyCompiled() => _empty.Compiled(default);
                                 ^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c8d904 4883c110        add     rcx,10h
00007ffd`39c8d908 488b4108        mov     rax,qword ptr [rcx+8]
00007ffd`39c8d90c 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`39c8d910 33d2            xor     edx,edx
00007ffd`39c8d912 ff5018          call    qword ptr [rax+18h]
00007ffd`39c8d915 90              nop

</code></pre></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr><th colspan="2">Delegates.ExpressionDeclared</th></tr>
<tr>
<th>.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0</th>
<th>.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align:top;"><pre><code>
00007ffd`4f8e0f20 StackOverflow.Performance.Delegates.Delegates.ExpressionDeclared()
		public void ExpressionDeclared() => _expression.Declared(_message);
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8e0f24 488d5120        lea     rdx,[rcx+20h]
00007ffd`4f8e0f28 488b02          mov     rax,qword ptr [rdx]
00007ffd`4f8e0f2b 488b5108        mov     rdx,qword ptr [rcx+8]
00007ffd`4f8e0f2f 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`4f8e0f33 ff5018          call    qword ptr [rax+18h]
00007ffd`4f8e0f36 90              nop

</code></pre></td>
<td style="vertical-align:top;"><pre><code>
00007ffd`39c9d930 StackOverflow.Performance.Delegates.Delegates.ExpressionDeclared()
		public void ExpressionDeclared() => _expression.Declared(_message);
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c9d934 488d5120        lea     rdx,[rcx+20h]
00007ffd`39c9d938 488b02          mov     rax,qword ptr [rdx]
00007ffd`39c9d93b 488b5108        mov     rdx,qword ptr [rcx+8]
00007ffd`39c9d93f 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`39c9d943 ff5018          call    qword ptr [rax+18h]
00007ffd`39c9d946 90              nop

</code></pre></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr><th colspan="2">Delegates.ExpressionCompiled</th></tr>
<tr>
<th>.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0</th>
<th>.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align:top;"><pre><code>
00007ffd`4f8f0f70 StackOverflow.Performance.Delegates.Delegates.ExpressionCompiled()
		public void ExpressionCompiled() => _expression.Compiled(_message);
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8f0f74 488d5120        lea     rdx,[rcx+20h]
00007ffd`4f8f0f78 488b4208        mov     rax,qword ptr [rdx+8]
00007ffd`4f8f0f7c 488b5108        mov     rdx,qword ptr [rcx+8]
00007ffd`4f8f0f80 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`4f8f0f84 ff5018          call    qword ptr [rax+18h]
00007ffd`4f8f0f87 90              nop

</code></pre></td>
<td style="vertical-align:top;"><pre><code>
00007ffd`39c9d980 StackOverflow.Performance.Delegates.Delegates.ExpressionCompiled()
		public void ExpressionCompiled() => _expression.Compiled(_message);
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c9d984 488d5120        lea     rdx,[rcx+20h]
00007ffd`39c9d988 488b4208        mov     rax,qword ptr [rdx+8]
00007ffd`39c9d98c 488b5108        mov     rdx,qword ptr [rcx+8]
00007ffd`39c9d990 488b4808        mov     rcx,qword ptr [rax+8]
00007ffd`39c9d994 ff5018          call    qword ptr [rax+18h]
00007ffd`39c9d997 90              nop

</code></pre></td>
</tr>
</tbody>
</table>
&#13;
&#13;
&#13;

看来,声明的和已编译的委托反汇编之间的唯一区别是声明的rcx与在其各自的第一个rcx+8操作中使用的编译的mov之间的区别。我还没有在拆卸方面说得好,所以非常感谢能够了解这个问题。乍一看,这似乎不会导致差异/改进,如果是这样,原生声明的委托也应该具有它(所以换句话说,一个错误)。

陈述了所有这些,对我来说显而易见的问题是:

  1. 这是一个已知问题和/或错误吗?
  2. 我在这里完全离开了吗? (猜猜这应该是第一个问题。:))
  3. 那么指导是否始终尽可能使用已编译的代理人?正如我之前提到的,似乎在编译的委托中发生的魔法已经被烘焙到声明的委托中,所以这有点令人困惑。
  4. 为了完整性,以下是此处示例中使用的所有代码:

    sealed class Program
    {
        static void Main()
        {
            BenchmarkRunner.Run<Delegates>();
        }
    }
    
    [CoreJob, ClrJob, DisassemblyDiagnoser(true, printSource: true)]
    public class Delegates
    {
        readonly DelegatePair<string, string> _empty;
        readonly DelegatePair<string, int>    _expression;
        readonly string                       _message;
    
        public Delegates() : this(new DelegatePair<string, string>(_ => default, _ => default),
                                  new DelegatePair<string, int>(x => x.Length, x => x.Length)) {}
    
        public Delegates(DelegatePair<string, string> empty, DelegatePair<string, int> expression,
                         string message = "Hello World!")
        {
            _empty      = empty;
            _expression = expression;
            _message    = message;
            EmptyDeclared();
            EmptyCompiled();
            ExpressionDeclared();
            ExpressionCompiled();
        }
    
        [Benchmark]
        public void EmptyDeclared() => _empty.Declared(default);
    
        [Benchmark]
        public void EmptyCompiled() => _empty.Compiled(default);
    
        [Benchmark]
        public void ExpressionDeclared() => _expression.Declared(_message);
    
        [Benchmark]
        public void ExpressionCompiled() => _expression.Compiled(_message);
    }
    
    public struct DelegatePair<TFrom, TTo>
    {
        DelegatePair(Func<TFrom, TTo> declared, Func<TFrom, TTo> compiled)
        {
            Declared = declared;
            Compiled = compiled;
        }
    
        public DelegatePair(Func<TFrom, TTo> declared, Expression<Func<TFrom, TTo>> expression) :
            this(declared, expression.Compile()) {}
    
        public Func<TFrom, TTo> Declared { get; }
    
        public Func<TFrom, TTo> Compiled { get; }
    }
    

    提前感谢您提供的任何帮助!

1 个答案:

答案 0 :(得分:7)

  

我在这里完全离开了吗? (猜猜这应该是第一个问题。:))

我合理地确定您所看到的反汇编仅适用于基准测试方法:加载委托及其参数所需的指令,然后调用委托。它包含每个代表的主体。

这就是为什么唯一的区别是其中一个mov指令的相对偏移:其中一个委托在结构中偏移0处,另一个居住在偏移量8.交换声明CompiledDeclared的顺序,并查看反汇编的更改方式。

我不知道有任何办法让Benchmark.NET为调用树中更深层次的调用吐出反汇编。文档建议将recursiveDepth设置为n > 1上的某个值[DisassemblyDiagnoser]应该这样做,但在这种情况下它似乎不起作用。

  

你是说我们没有看到额外的反汇编?

正确,您没有看到委托机构的反汇编。如果编辑它们的方式不同,那就是它可见的地方。

  

您是说我们没有看到额外的拆卸?由于两个机构完全相同(或者至少看起来是相同的),我还不清楚这是怎么回事。

尸体不一定相同。对于基于Expression的lambas,C#编译器不会为描述的表达式发出IL;相反,它会发出一系列Expression工厂调用来在运行时构造表达式树。该表达式树描述的代码应该功能上等同于生成它的C#表达式,但它在运行时由LambdaCompiler在调用Compile()时编译。 LINQ表达式树意味着与语言无关,并且不一定与C#编译器生成的表达式完全相同。因为lambda表达式是由不同的(并且不太复杂的)编译器编译的,所以得到的IL可能与C#编译器发出的有点不同。例如,lambda编译器倾向于发出比C#编译器更多的临时本地,或者至少它是我最后一次在源代码中搜索时所做的。

确定每个代表的实际反汇编的最佳选择可能是在调试器中加载SOS.dll。我试图自己这样做,但我似乎无法弄清楚如何让它在VS2017中运行。我从来没有遇到过麻烦。我还没有完全接受VS2017中的新项目模型,并且无法弄清楚如何启用非托管调试。

好的,我把SOS.dll加载了WinDbg,经过一段谷歌搜索,我现在能够查看IL和反汇编。首先,让我们看一下lambda体的方法描述符。这是已声明的版本:

0:000> !DumpMD 000007fe97686148

Method Name:  StackOverflow.Performance.Delegates.Delegates+<>c.<.ctor>b__3_2(System.String)
Class:        000007fe977d14d0
MethodTable:  000007fe97686158
mdToken:      000000000600000e
Module:       000007fe976840c0
IsJitted:     yes
CodeAddr:     000007fe977912b0
Transparency: Critical

这是已编译版本:

0:000> !DumpMD 000007fe97689390

Method Name:  DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.String)
Class:        000007fe97689270
MethodTable:  000007fe976892e8
mdToken:      0000000006000000
Module:       000007fe97688af8
IsJitted:     yes
CodeAddr:     000007fe977e0150
Transparency: Transparent

我们可以转储IL并看到它实际上是相同的:

0:000> !DumpIL 000007fe97686148

IL_0000: ldarg.1 
IL_0001: callvirt 6000002 System.String.get_Length()
IL_0006: ret 

0:000> !DumpIL 000007fe97689390

IL_0000: ldarg.1 
IL_0001: callvirt System.String::get_Length 
IL_0006: ret

反过来也是反汇编:

0:000> !U 000007fe977912b0

Normal JIT generated code
StackOverflow.Performance.Delegates.Delegates+<>c.<.ctor>b__3_2(System.String)
Begin 000007fe977912b0, size 4
W:\dump\DelegateBenchmark\StackOverflow.Performance.Delegates\Delegates.cs @ 14:

000007fe`977912b0 8b4208          mov     eax,dword ptr [rdx+8]
000007fe`977912b3 c3              ret

0:000> !U 000007fe977e0150

Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.String)
Begin 000007fe977e0150, size 4

000007fe`977e0150 8b4208          mov     eax,dword ptr [rdx+8]
000007fe`977e0153 c3              ret

所以,我们有相同的IL和相同的程序集。 差异来自何处?让我们来看看实际的委托实例。通过这个,我不是指lambda体,而是我们用来调用lambda的Delegate个对象。

0:000> !DumpVC /d 000007fe97686040 0000000002a84410

Name:        StackOverflow.Performance.Delegates.DelegatePair`2[[System.String, mscorlib],[System.Int32, mscorlib]]
MethodTable: 000007fe97686040
EEClass:     000007fe977d12d0
Size:        32(0x20) bytes
File:        W:\dump\DelegateBenchmark\StackOverflow.Performance.Delegates\bin\Release\net461\StackOverflow.Performance.Delegates.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fef692e400  4000001        0 ...Int32, mscorlib]]  0 instance 0000000002a8b4d8 <Declared>k__BackingField
000007fef692e400  4000002        8 ...Int32, mscorlib]]  0 instance 0000000002a8d3f8 <Compiled>k__BackingField

我们有两个委托值:在我的情况下,Declared位于02a8b4d8,而Compiled位于02a8d3f8(这些地址对我的流程而言是唯一的)。如果我们使用!DumpObject转储每个地址并查找_methodPtr值,我们可以看到已编译方法的地址。然后我们可以使用!U转储程序集:

0:000> !U 7fe977e0150 

Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.String)
Begin 000007fe977e0150, size 4

000007fe`977e0150 8b4208          mov     eax,dword ptr [rdx+8]
000007fe`977e0153 c3              ret

好的,对于Compiled,我们可以看到我们直接调用lambda体。尼斯。但是当我们转储Declared版本的反汇编时,我们会看到不同的东西:

0:000> !U 7fe977901d8 

Unmanaged code

000007fe`977901d8 e8f326635f      call    clr!PrecodeFixupThunk (000007fe`f6dc28d0)
000007fe`977901dd 5e              pop     rsi
000007fe`977901de 0400            add     al,0
000007fe`977901e0 286168          sub     byte ptr [rcx+68h],ah
000007fe`977901e3 97              xchg    eax,edi
000007fe`977901e4 fe07            inc     byte ptr [rdi]
000007fe`977901e6 0000            add     byte ptr [rax],al
000007fe`977901e8 0000            add     byte ptr [rax],al
000007fe`977901ea 0000            add     byte ptr [rax],al
000007fe`977901ec 0000            add     byte ptr [rax],al

你好。我记得在blog post by Matt Warren中看到clr!PrecodeFixupThunk的引用。我的理解是,普通 IL方法的入口点(与基于LINQ的方法的动态方法相反)调用了一个调用JIT的修复方法第一次调用,然后在后续调用时调用JITed方法。这个&#39; thunk&#39;的额外开销。在调用“宣布的”#39;代表似乎是原因。 &#39;编译&#39;代表没有这样的蠢事;委托直接指向已编译的lambda体。