为什么编译委托比声明委托更快?
Why Is a Compiled Delegate Faster Than a Declared Delegate?
首先,这与 Why is Func<> created from Expression> slower than Func<> declared directly? 不同,令人惊讶的是恰恰相反。此外,我在研究这个问题时发现的所有链接和问题都源于 2010-2012 时间段,所以我决定在这里提出一个新问题,看看是否有一些关于代表当前状态的讨论.NET 生态系统中的行为。
就是说,我正在使用 .NET Core 2.0 和 .NET 4.7.1,并且我看到了一些关于从编译表达式创建的委托与描述和声明为 CLR 对象的委托的奇怪性能指标.
关于我是如何偶然发现这个问题的一些上下文,我正在做一个测试,涉及在 1,000 和 10,000 个对象的数组中选择数据,并注意到如果我使用编译表达式,它会在整个过程中获得更快的结果木板。我设法将其归结为一个非常简单的项目,它重现了您可以在这里找到的这个问题:
https://github.com/Mike-EEE/Whosebug.Performance.Delegates
为了进行测试,我使用了两组基准测试,其中包含一个已编译的委托与一个已声明的委托配对,总共有四个核心基准。
第一个委托集由一个空委托组成,returns 一个空字符串。第二组是一个委托,其中有一个简单的表达式。我想证明这个问题发生在最简单的委托以及其中有定义主体的委托中。
这些测试然后 运行 在 CLR 运行 时间和 .NET Core 运行 时间通过优秀的 Benchmark.NET performance product, resulting in eight total benchmarks. Additionally, I also make use of the just-as-excellent 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 |
请注意,使用编译委托的基准测试始终更快。
最后,这是每个基准测试遇到的反汇编结果:
<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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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>
声明的和编译的委托反汇编之间的唯一区别似乎是声明的 rcx
与在各自的第一个 mov
操作中使用的编译的 rcx+8
。我在反汇编方面还不是那么好,所以非常感谢了解这个问题的背景。乍一看,这似乎不会导致 difference/improvement,如果是这样,本机声明的委托也应该具有它(换句话说,一个错误)。
综上所述,对我来说显而易见的问题是:
- 这是一个已知问题 and/or 错误吗?
- 我是不是在做一些完全偏离基地的事情? (猜猜这应该是第一个问题。:))
- 那么指南是否总是尽可能使用已编译的委托?正如我之前提到的,编译委托中发生的魔法似乎已经融入声明的委托中,所以这有点令人困惑。
为了完整起见,这里完整展示了示例中使用的所有代码:
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; }
}
提前感谢您提供的任何帮助!
Am I doing something entirely off-base here? (Guess this should be the first question. :))
我有理由确定您看到的反汇编仅针对基准方法:加载委托及其参数所需的指令,然后调用委托。它不包括每个代表的body。
这就是为什么唯一的区别是 mov
指令之一的相对偏移量:其中一个委托位于结构中的偏移量 0,而另一个位于偏移量 8。交换声明顺序Compiled
和 Declared
,看看反汇编如何变化。
我不知道有什么方法可以让 Benchmark.NET 吐出调用树中更深层调用的反汇编。文档建议在 [DisassemblyDiagnoser]
上将 recursiveDepth
设置为某个值 n > 1
应该可以,但在这种情况下似乎不起作用。
Are you saying there is extra disassembly that we are not seeing?
正确,您没有看到委托主体的反汇编。如果它们的编译方式有所不同,那就是可见的地方。
Are you saying there is extra disassembly that we are not seeing? Since both bodies are exactly the same (or at least, appear to be the same), I am further unclear on how this would be the case.
身体不一定相同。对于基于 Expression
的 lambas,C# 编译器不会为 described 表达式发出 IL;相反,它发出一系列 Expression
工厂调用以在运行时构建表达式树。该表达式树描述的代码应该 在功能上等同于 生成它的 C# 表达式,但它是由 LambdaCompiler
在运行时调用 Compile()
编译的。 LINQ 表达式树是 language-agnostic,不一定与 C# 编译器生成的表达式完全相同。由于 lambda 表达式是由不同的(并且不太复杂)编译器编译的,因此生成的 IL 可能与 C# 编译器发出的内容略有不同。例如,lambda 编译器往往会发出比 C# 编译器更多的临时局部变量,或者至少在我上次查看源代码时它是这样。
确定每个委托的实际反汇编的最佳方法可能是在调试器中加载 SOS.dll。我自己尝试这样做,但我似乎无法弄清楚如何让它在 VS2017 中工作。我过去从来没有遇到过麻烦。我还没有完全适应 VS2017 中的新项目模型,也不知道如何启用非托管调试。
好的,我 SOS.dll 加载了 WinDbg,经过一些谷歌搜索后,我现在可以查看 IL 和反汇编。首先,让我们看一下 lambda 主体的方法描述符。这是声明的版本:
0:000> !DumpMD 000007fe97686148
Method Name: Whosebug.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
Whosebug.Performance.Delegates.Delegates+<>c.<.ctor>b__3_2(System.String)
Begin 000007fe977912b0, size 4
W:\dump\DelegateBenchmark\Whosebug.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
objects。
0:000> !DumpVC /d 000007fe97686040 0000000002a84410
Name: Whosebug.Performance.Delegates.DelegatePair`2[[System.String, mscorlib],[System.Int32, mscorlib]]
MethodTable: 000007fe97686040
EEClass: 000007fe977d12d0
Size: 32(0x20) bytes
File: W:\dump\DelegateBenchmark\Whosebug.Performance.Delegates\bin\Release\net461\Whosebug.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 body。好的。但是当我们转储 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
的引用。我的理解是,normal IL 方法(与 dynamic 方法(如我们的 LINQ-based 方法相反)的入口点调用一个修复方法,它在第一次调用时调用 JIT,然后在后续调用中调用 JITed 方法。调用 'declared' 委托时 'thunk' 的额外开销似乎是原因。 'compiled' 代表没有这样的声音;委托直接指向已编译的 lambda body.
首先,这与 Why is Func<> created from Expression> slower than Func<> declared directly? 不同,令人惊讶的是恰恰相反。此外,我在研究这个问题时发现的所有链接和问题都源于 2010-2012 时间段,所以我决定在这里提出一个新问题,看看是否有一些关于代表当前状态的讨论.NET 生态系统中的行为。
就是说,我正在使用 .NET Core 2.0 和 .NET 4.7.1,并且我看到了一些关于从编译表达式创建的委托与描述和声明为 CLR 对象的委托的奇怪性能指标.
关于我是如何偶然发现这个问题的一些上下文,我正在做一个测试,涉及在 1,000 和 10,000 个对象的数组中选择数据,并注意到如果我使用编译表达式,它会在整个过程中获得更快的结果木板。我设法将其归结为一个非常简单的项目,它重现了您可以在这里找到的这个问题:
https://github.com/Mike-EEE/Whosebug.Performance.Delegates
为了进行测试,我使用了两组基准测试,其中包含一个已编译的委托与一个已声明的委托配对,总共有四个核心基准。
第一个委托集由一个空委托组成,returns 一个空字符串。第二组是一个委托,其中有一个简单的表达式。我想证明这个问题发生在最简单的委托以及其中有定义主体的委托中。
这些测试然后 运行 在 CLR 运行 时间和 .NET Core 运行 时间通过优秀的 Benchmark.NET performance product, resulting in eight total benchmarks. Additionally, I also make use of the just-as-excellent 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 |
请注意,使用编译委托的基准测试始终更快。
最后,这是每个基准测试遇到的反汇编结果:
<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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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 Whosebug.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>
声明的和编译的委托反汇编之间的唯一区别似乎是声明的 rcx
与在各自的第一个 mov
操作中使用的编译的 rcx+8
。我在反汇编方面还不是那么好,所以非常感谢了解这个问题的背景。乍一看,这似乎不会导致 difference/improvement,如果是这样,本机声明的委托也应该具有它(换句话说,一个错误)。
综上所述,对我来说显而易见的问题是:
- 这是一个已知问题 and/or 错误吗?
- 我是不是在做一些完全偏离基地的事情? (猜猜这应该是第一个问题。:))
- 那么指南是否总是尽可能使用已编译的委托?正如我之前提到的,编译委托中发生的魔法似乎已经融入声明的委托中,所以这有点令人困惑。
为了完整起见,这里完整展示了示例中使用的所有代码:
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; }
}
提前感谢您提供的任何帮助!
Am I doing something entirely off-base here? (Guess this should be the first question. :))
我有理由确定您看到的反汇编仅针对基准方法:加载委托及其参数所需的指令,然后调用委托。它不包括每个代表的body。
这就是为什么唯一的区别是 mov
指令之一的相对偏移量:其中一个委托位于结构中的偏移量 0,而另一个位于偏移量 8。交换声明顺序Compiled
和 Declared
,看看反汇编如何变化。
我不知道有什么方法可以让 Benchmark.NET 吐出调用树中更深层调用的反汇编。文档建议在 [DisassemblyDiagnoser]
上将 recursiveDepth
设置为某个值 n > 1
应该可以,但在这种情况下似乎不起作用。
Are you saying there is extra disassembly that we are not seeing?
正确,您没有看到委托主体的反汇编。如果它们的编译方式有所不同,那就是可见的地方。
Are you saying there is extra disassembly that we are not seeing? Since both bodies are exactly the same (or at least, appear to be the same), I am further unclear on how this would be the case.
身体不一定相同。对于基于 Expression
的 lambas,C# 编译器不会为 described 表达式发出 IL;相反,它发出一系列 Expression
工厂调用以在运行时构建表达式树。该表达式树描述的代码应该 在功能上等同于 生成它的 C# 表达式,但它是由 LambdaCompiler
在运行时调用 Compile()
编译的。 LINQ 表达式树是 language-agnostic,不一定与 C# 编译器生成的表达式完全相同。由于 lambda 表达式是由不同的(并且不太复杂)编译器编译的,因此生成的 IL 可能与 C# 编译器发出的内容略有不同。例如,lambda 编译器往往会发出比 C# 编译器更多的临时局部变量,或者至少在我上次查看源代码时它是这样。
确定每个委托的实际反汇编的最佳方法可能是在调试器中加载 SOS.dll。我自己尝试这样做,但我似乎无法弄清楚如何让它在 VS2017 中工作。我过去从来没有遇到过麻烦。我还没有完全适应 VS2017 中的新项目模型,也不知道如何启用非托管调试。
好的,我 SOS.dll 加载了 WinDbg,经过一些谷歌搜索后,我现在可以查看 IL 和反汇编。首先,让我们看一下 lambda 主体的方法描述符。这是声明的版本:
0:000> !DumpMD 000007fe97686148
Method Name: Whosebug.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
Whosebug.Performance.Delegates.Delegates+<>c.<.ctor>b__3_2(System.String)
Begin 000007fe977912b0, size 4
W:\dump\DelegateBenchmark\Whosebug.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
objects。
0:000> !DumpVC /d 000007fe97686040 0000000002a84410
Name: Whosebug.Performance.Delegates.DelegatePair`2[[System.String, mscorlib],[System.Int32, mscorlib]]
MethodTable: 000007fe97686040
EEClass: 000007fe977d12d0
Size: 32(0x20) bytes
File: W:\dump\DelegateBenchmark\Whosebug.Performance.Delegates\bin\Release\net461\Whosebug.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 body。好的。但是当我们转储 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
的引用。我的理解是,normal IL 方法(与 dynamic 方法(如我们的 LINQ-based 方法相反)的入口点调用一个修复方法,它在第一次调用时调用 JIT,然后在后续调用中调用 JITed 方法。调用 'declared' 委托时 'thunk' 的额外开销似乎是原因。 'compiled' 代表没有这样的声音;委托直接指向已编译的 lambda body.