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

如果我创建 A 的一个新实例,访问 Y 100,000,000 次大约需要 550 毫秒,而访问 X 大约需要 250 毫秒。我将它作为发布版本运行,但对于该属性来说它仍然慢得多。为什么 .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();

那是我用来测试的代码,我将 tY 更改为 tX 来测试 X。我也在发布版本中。

4

2 回答 2

28
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

完全一样的时间。当然,应该是这样的。您无需为财产付费。

抖动在生成高质量的机器代码方面做得很好。如果你得到一个奇怪的结果,那么总是先检查你的测试代码。这是最有可能出错的代码。不是抖动,是经过彻底测试的。

于 2013-03-16T21:48:49.937 回答
5

X只是一个简单的字段。然而Y是一个带有getset访问器的属性,命名 int get_Y()void set_Y(int)内部。还有一个带有特殊编译器生成名称的私有支持字段,访问器访问支持字段。Y实践中显示的下图:

uplyZ.jpg

根据 C# 语言规范,编译器应该这样做。如果 C# 编译器改为发出一个字段,它将违反规范。

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

Hans Passant强调,实际上运行时以同样快的速度进行属性访问。您的原始测试代码有缺陷,运行时可以删除读取,因为它分配给的局部变量从未使用过。详细参见 Passant 的回答。

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

于 2013-03-16T20:46:01.743 回答