为什么设置字段比获取字段慢很多倍

本文关键字:字段 获取 设置 为什么 | 更新日期: 2023-09-27 18:31:49

我已经知道设置字段比设置局部变量慢得多,但似乎使用局部变量设置字段比使用字段设置局部变量要慢得多。这是为什么呢?在任一情况下,都使用该字段的地址。

public class Test
{
    public int A = 0;
    public int B = 4;
    public void Method1() // Set local with field
    {
        int a = A;
        for (int i = 0; i < 100; i++)
        {
            a += B;
        }
        A = a;
    }
    public void Method2() // Set field with local
    {
        int b = B;
        for (int i = 0; i < 100; i++)
        {
            A += b;
        }
    }
}

10e+6 次迭代的基准测试结果为:

方法1: 28.1321 ms方法2: 162.4528 ms

为什么设置字段比获取字段慢很多倍

在我的机器上运行它,我得到了类似的时差,但是查看 10M 迭代的 JITted 代码,很明显为什么会这样:

方法一:

mov     r8,rcx
; "A" is loaded into eax
mov     eax,dword ptr [r8+8]
xor     edx,edx
; "B" is loaded into ecx
mov     ecx,dword ptr [r8+0Ch]
nop     dword ptr [rax]
loop_start:
; Partially unrolled loop, all additions done in registers
add     eax,ecx
add     eax,ecx
add     eax,ecx
add     eax,ecx
add     edx,4
cmp     edx,989680h
jl      loop_start
; Store the sum in eax back to "A"
mov     dword ptr [r8+8],eax
ret

和方法B:

; "B" is loaded into edx
mov     edx,dword ptr [rcx+0Ch]
xor     r8d,r8d
nop word ptr [rax+rax]
loop_start:
; Partially unrolled loop, but each iteration requires reading "A" from memory
; adding "B" to it, and then writing the new "A" back to memory.
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
add     r8d,4
cmp     r8d,989680h
jl      loop_start
rep ret

从程序集中可以看出,方法 A 将明显更快,因为 A 和 B 的值都放在寄存器中,并且所有添加都发生在那里,没有中间写入内存。另一方面,方法 B 在每次迭代时都会产生负载并存储到内存中的"A"。

在 1 的情况下,a清楚地存储在寄存器中。其他任何事情都将是一个可怕的编译结果。

在情况 2 中,.NET JIT 可能不愿意/无法将存储转换为A以注册存储。

我怀疑这是 .NET 内存模型强制的,因为如果其他线程只观察到A为 0 或总和,它们就永远无法分辨出您的两种方法之间的区别。他们无法反驳优化从未发生过的理论。这使得它在 .NET 抽象机器的语义下是允许的。

看到 .NET JIT 执行少量优化并不奇怪。这是Stack Overflow上performance标签的追随者所熟知的。

我从经验中知道,JIT 更有可能在寄存器中缓存内存负载。这就是为什么情况 1(显然)每次迭代都无法访问B的原因。

寄存器计算比内存访问更便宜。如果所讨论的内存位于 CPU L1 缓存中(如此处的情况),则甚至如此。

我以为只有当地人才有资格使用 CPU 缓存?

这不可能是这样,因为 CPU 甚至不知道本地是什么。所有地址看起来都一样。

方法2:字段被读取~100倍并设置~100倍 = 200倍larg_0(这个)+ 100倍LDFLD(加载场)+ 100倍STFLD(设置字段)+ 100倍LDLOC(本地)

方法1:字段读取100倍,但未设置相当于方法1减去100倍ldarg_0(这)