sealed class A
{
public int X;
public int Y { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
如果我创建一个新的A实例,它需要大约550ms才能访问Y 100,000,000次,而访问X大约需要250ms.我将它作为发布版本运行,它对于该属性来说仍然要慢得多.为什么.NET不优化Y到字段?
编辑:
A t = new A();
t.Y = 50;
t.X = 50;
Int64 y = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100000000; i++)
y += t.Y;
sw.Stop();
Run Code Online (Sandbox Code Playgroud)
那是我用来测试的代码,我正在改变tY到tX来测试X. 我也在发布版本中.
Han*_*ant 25
for (int i = 0; i < 100000000; i++)
y += t.X;
Run Code Online (Sandbox Code Playgroud)
这是非常难以分析的代码.使用Debug + Windows + Disassembly查看生成的机器代码时,您可以看到.x64代码如下所示:
0000005a xor r11d,r11d ; i = 0
0000005d mov eax,dword ptr [rbx+0Ch] ; read t.X
00000060 add r11d,4 ; i += 4
00000064 cmp r11d,5F5E100h ; test i < 100000000
0000006b jl 0000000000000060 ; for (;;)
Run Code Online (Sandbox Code Playgroud)
这是经过大量优化的代码,请注意+ =运算符如何完全消失.你允许这种情况发生,因为你在基准测试中犯了一个错误,你根本没有使用y的计算值.抖动知道这一点,所以它简单地删除了无意义的添加.增量4也需要解释,这是循环展开优化的副作用.你会看到以后使用它.
因此,您必须对基准进行更改以使其成为现实,最后添加此行:
sw.Stop();
Console.WriteLine("{0} msec, {1}", sw.ElapsesMilliseconds, y);
Run Code Online (Sandbox Code Playgroud)
这会强制计算y的值.它现在看起来完全不同:
0000005d xor ebp,ebp ; y = 0
0000005f mov eax,dword ptr [rbx+0Ch]
00000062 movsxd rdx,eax ; rdx = t.X
00000065 nop word ptr [rax+rax+00000000h] ; align branch target
00000070 lea rax,[rdx+rbp] ; y += t.X
00000074 lea rcx,[rax+rdx] ; y += t.X
00000078 lea rax,[rcx+rdx] ; y += t.X
0000007c lea rbp,[rax+rdx] ; y += t.X
00000080 add r11d,4 ; i += 4
00000084 cmp r11d,5F5E100h ; test i < 100000000
0000008b jl 0000000000000070 ; for (;;)
Run Code Online (Sandbox Code Playgroud)
仍然非常优化的代码.奇怪的NOP指令确保地址008b处的跳转是有效的,跳转到与16对齐的地址优化处理器中的指令解码器单元.LEA指令是让地址生成单元生成添加的经典技巧,允许主ALU同时执行其他工作.没有其他工作要做,但如果循环体更多涉及可能有.并且循环展开4次以避免分支指令.
Anyhoo,现在你实际上在测量实际代码,而不是删除代码.结果在我的机器上,重复测试10次(重要!):
y += t.X: 125 msec
y += t.Y: 125 msec
Run Code Online (Sandbox Code Playgroud)
完全相同的时间.当然,它应该是这样的.您不需要支付房产费用.
抖动可以很好地生成高质量的机器代码.如果你得到一个奇怪的结果,那么总是先检查你的测试代码.这是最容易出错的代码.不是抖动,它已经过彻底测试.
X只是一个简单的字段。然而Y是一个带有get和set访问器的属性,命名
int get_Y()和void set_Y(int)内部。还有一个私有的支持字段,Y带有一个特殊的编译器生成的名称,访问者访问支持字段。在实践中显示以下图像:

根据 C# 语言规范,这就是编译器应该这样做的方式。如果 C# 编译器改为发出一个字段,则会违反规范。
当然,运行时必须使用编译器生成的访问器。但是运行时可能会做内联之类的技巧来避免对访问器的额外调用。这是一种优化,可以使属性访问与字段访问一样快。
Hans Passant强调,实际上运行时会以同样快的速度进行属性访问。您的原始测试代码有缺陷,运行时可以删除读取,因为它分配给的局部变量从未使用过。详细参见Passant的回答。
不过,如果您想要一个普通字段,请写一个,并且不要创建自动属性。