为什么不对字段优化的简单属性?

sealed class A { public int X; public int Y { get; set; } } 

如果我创建一个新的A实例,我需要大约550ms来访问Y 100,000,000次,而访问X大约需要250ms。我将它作为发布版本运行,它对于该属性来说仍然要慢得多。 为什么.NET不优化Y到字段?

编辑:

  A t = new A(); tY = 50; tX = 50; Int64 y = 0; Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 100000000; i++) y += tY; sw.Stop(); 

这是我用来测试的代码,我正在改变tY到tX来测试X. 我也在发布版本。

 for (int i = 0; i < 100000000; i++) y += tX; 

这是非常难以分析的代码。 使用Debug + Windows + Disassembly查看生成的机器代码时可以看到。 x64代码如下所示:

 0000005a xor r11d,r11d ; i = 0 0000005d mov eax,dword ptr [rbx+0Ch] ; read tX 00000060 add r11d,4 ; i += 4 00000064 cmp r11d,5F5E100h ; test i < 100000000 0000006b jl 0000000000000060 ; for (;;) 

这是经过大量优化的代码,请注意+ =运算符如何完全消失。 您允许这种情况发生,因为您在基准测试中犯了一个错误,您根本没有使用y的计算值。 抖动知道这一点,所以它简单地删除了无意义的添加。 增量4也需要解释,这是循环展开优化的副作用。 你会看到它以后使用。

因此,您必须对基准测试进行更改以使其更加真实,最后添加此行:

 sw.Stop(); Console.WriteLine("{0} msec, {1}", sw.ElapsesMilliseconds, y); 

这会强制计算y的值。 它现在看起来完全不同:

 0000005d xor ebp,ebp ; y = 0 0000005f mov eax,dword ptr [rbx+0Ch] 00000062 movsxd rdx,eax ; rdx = tX 00000065 nop word ptr [rax+rax+00000000h] ; align branch target 00000070 lea rax,[rdx+rbp] ; y += tX 00000074 lea rcx,[rax+rdx] ; y += tX 00000078 lea rax,[rcx+rdx] ; y += tX 0000007c lea rbp,[rax+rdx] ; y += tX 00000080 add r11d,4 ; i += 4 00000084 cmp r11d,5F5E100h ; test i < 100000000 0000008b jl 0000000000000070 ; for (;;) 

仍然非常优化的代码。 奇怪的NOP指令确保地址008b处的跳转是有效的,跳转到与16对齐的地址优化处理器中的指令解码器单元。 LEA指令是让地址生成单元生成添加的经典技巧,允许主ALU同时执行其他工作。 没有其他工作要做,但如果循环体更多参与可能会有。 并且循环展开4次以避免分支指令。

Anyhoo,现在你实际上在测量实际代码,而不是删除代码。 结果在我的机器上,重复测试10次(重要!):

 y += tX: 125 msec y += tY: 125 msec 

完全相同的时间。 当然,它应该是那样的。 您不需要支付房产费用。

抖动可以很好地生成高质量的机器代码。 如果您得到一个奇怪的结果,那么请先检查您的测试代码。 这是最容易出错的代码。 不是抖动,它已经过彻底测试。

X只是一个简单的领域。 但是, Y是具有getset访问器的属性,在内部命名为int get_Y()void set_Y(int)Y还有一个私有支持字段 ,具有特殊的编译器生成名称,访问者访问支持字段。 实践中显示的Follwoing图像:

uplyZ.jpg

根据C#语言规范,这就是编译器应该如何做到的。 如果C#编译器发出了一个字段,那么它将违反规范。

当然,运行时必须使用编译器生成的访问器。 但是运行时可能会做内联的技巧,以避免对访问者的额外调用。 这是一种优化, 可以使属性访问与现场访问一样快。

Hans Passant强调,实际上运行时将以尽可能快的速度访问属性。 您的原始测试代码存在缺陷,运行时可能会删除读取,因为它所分配的局部变量从未使用过。 详细了解Passant的答案。

但是,如果你想要一个普通字段,写一个,不要创建一个自动属性。

Befor我发布,你有两个答案让你了解属性是如何工作的,以及优化会是什么样的。

由于优化策略各不相同 ,因此无法向您展示不变的优化。 预测代码期望的内容有限但也有各种方法。

属性通过调用方法来使用方法,以反映实际数据存储位置(例如字段)的更改。 方法调用也可能有各种方法在不同的上下文中进行优化。

如果属性总是作为字段进行优化,也就是说,您始终首先选择相同的策略,并且可能会在不同的上下文中丢失其他策略。