C# 为什么编译委托比声明委托快?
首先,这与它不同,令人惊讶的是,它正好相反。此外,我在研究这个问题时发现的所有链接和问题都源自2010-2012年的时间段,因此我决定在这里提出一个新问题,看看是否有一些关于.NET生态系统中代理行为的当前状态的讨论 这就是说,我使用的是.NET Core 2.0和.NET 4.7.1,我看到了一些奇怪的性能指标,这些指标与从编译表达式创建的委托以及描述和声明为CLR对象的委托有关 为了了解我是如何偶然发现这个问题的,我做了一个测试,测试中选择了1000和10000个对象的数组中的数据,并注意到如果我使用编译后的表达式,则会得到更快的结果。我设法将其归结为一个非常简单的项目,它再现了这个问题,您可以在这里找到: 对于测试,我使用了两组基准测试,它们的特点是一个已编译的委托与一个已声明的委托配对,结果总共有四个核心基准测试 第一个委托集由返回空字符串的空委托组成。第二个集合是一个委托,其中包含一个简单表达式。我想证明,这个问题发生在最简单的代理以及其中包含已定义主体的代理中 然后,通过卓越性能产品在CLR运行时和.NET核心运行时上运行这些测试,总共生成八个基准测试。此外,我还使用just as Perfective Benchmark.NET发出在基准测量JIT期间遇到的反汇编。我在下面分享这一结果 下面是运行基准测试的代码。你可以看到这是非常直截了当的:C# 为什么编译委托比声明委托快?,c#,.net,performance,delegates,linq-expressions,C#,.net,Performance,Delegates,Linq Expressions,首先,这与它不同,令人惊讶的是,它正好相反。此外,我在研究这个问题时发现的所有链接和问题都源自2010-2012年的时间段,因此我决定在这里提出一个新问题,看看是否有一些关于.NET生态系统中代理行为的当前状态的讨论 这就是说,我使用的是.NET Core 2.0和.NET 4.7.1,我看到了一些奇怪的性能指标,这些指标与从编译表达式创建的委托以及描述和声明为CLR对象的委托有关 为了了解我是如何偶然发现这个问题的,我做了一个测试,测试中选择了1000和10000个对象的数组中的数据,并注意到
[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);
}
请注意,使用已编译委托的基准测试始终更快
最后,以下是每个基准所遇到的反汇编结果:
表{边框折叠:折叠;显示:块;宽度:100%;溢出:自动;}
td,th{padding:6px 13px;border:1px solid#ddd;}
tr{背景色:#fff;边框顶部:1px实心#ccc;}
tr:n个孩子(偶数){背景:#f8f8;}
代理。已声明为空
.NET Framework 4.7.1(CLR 4.0.30319.42000),64位RyuJIT-v4.7.2633.0
.NET Core 2.0.7(CoreCLR 4.6.26328.01,CoreFX 4.6.26403.03),64位RyuJIT
Delegates.EmptyCompiled
.NET Framework 4.7.1(CLR 4.0.30319.42000),64位RyuJIT-v4.7.2633.0
.NET Core 2.0.7(CoreCLR 4.6.26328.01,CoreFX 4.6.26403.03),64位RyuJIT
degregates.expression声明
.NET Framework 4.7.1(CLR 4.0.30319.42000),64位RyuJIT-v4.7.2633.0
.NET Core 2.0.7(CoreCLR 4.6.26328.01,CoreFX 4.6.26403.03),64位RyuJIT
Delegates.ExpressionCompiled
.NET Framework 4.7.1(CLR 4.0.30319.42000),64位RyuJIT-v4.7.2633.0
.NET Core 2.0.7(CoreCLR 4.6.26328.01,CoreFX 4.6.26403.03),64位RyuJIT
我是不是在做一些完全离谱的事情?(猜这应该是第一个问题。:)
我可以合理地确定,您看到的反汇编只针对基准方法:加载委托及其参数,然后调用委托所需的指令。它不包括每个代理的主体
这就是为什么唯一的区别是其中一条mov
指令中的相对偏移量:一个委托位于结构中的偏移量0处,另一个位于偏移量8处。交换已编译的
和已声明的
的声明顺序,并查看反汇编是如何更改的
我不知道有什么方法可以让Benchmark.NET为调用树中更深层次的调用进行反汇编。文档建议将[DisassemblyDiagnoser]
上的recursiveDepth
设置为某个值n>1
,但在这种情况下似乎不起作用
你是说我们没有看到额外的拆卸吗 正确,您没有看到代理实体的反汇编。如果它们在编译方式上存在差异,那么就可以看到它们 你是说我们没有看到额外的拆卸吗?由于这两个机构是完全相同的(或至少看起来是相同的),我进一步不清楚情况会如何 身体不一定相同。对于基于
表达式的lambas,C#编译器不会为所描述的表达式发出IL;相反,它发出一系列Expression
factory调用,以在运行时构建表达式树。该表达式树描述的代码在功能上应该与生成它的C表达式等效,但它是由运行时调用Compile()
时由LambdaCompiler
编译的。LINQ表达式树意味着与语言无关,不一定与C#编译器生成的表达式具有精确的奇偶性。由于lambda表达式是由不同(且不太复杂)的编译器编译的,因此生成的IL可能与C#编译器发出的有点不同。例如,lambda编译器倾向于比C#编译器发出更多的临时局部变量,或者至少在我上次查看源代码时是这样
确定每个委托的实际反汇编的最佳方法可能是在调试器中加载。我自己也试过这么做,但我似乎不知道如何让它在VS2017中工作。我过去从来没有遇到过麻烦。我还没有完全适应VS2017中的新项目模型,也不知道如何启用非托管调试
好的,我用WinDbg加载了SOS.dll,在谷歌搜索了一下之后,我现在可以查看IL和反汇编了。首先,让我们看一下方法
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 |
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
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
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
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
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