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

时间:2013-03-16 20:39:27

标签: c# .net

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();
    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();

这是我用来测试的代码,我将t.Y更改为t.X来测试X.我也在发布版本。

2 个答案:

答案 0 :(得分:25)

for (int i = 0; i < 100000000; i++)
    y += t.X;

这是非常难以分析的代码。使用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 (;;)

这是经过大量优化的代码,请注意+ =运算符如何完全消失。你允许这种情况发生,因为你在你的基准测试中犯了一个错误,你根本没有使用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 = 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 (;;)

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

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

y += t.X: 125 msec
y += t.Y: 125 msec

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

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

答案 1 :(得分:4)

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

uplyZ.jpg

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

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

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

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