【问题标题】:Why is setting a field many times slower than getting a field?为什么设置字段比获取字段慢很多倍?
【发布时间】:2015-01-22 13:32:12
【问题描述】:

我已经知道设置字段比设置局部变量慢得多,但似乎with设置字段比设置局部变量要慢得多。为什么是这样?无论哪种情况,都使用该字段的地址。

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 毫秒 方法2:162.4528毫秒

【问题讨论】:

  • 这取决于很多事情,但最明显的解释是geting不一定要访问DRAM(值在CPU缓存中),而setting是(cache write-through.. .ie 值同时写入缓存和系统内存)。请注意,设置局部变量可能会导致根本无法访问内存,因为编译器可能已将局部变量优化为寄存器。
  • @PeterDuniho - 我以为只有本地人有资格获得 CPU 缓存?
  • 正如我在评论中提到的,本地人通常甚至不存储在系统 RAM 中。但是 all 内存访问,无论变量的类型如何,都符合缓存条件。缓存不关心(甚至不知道)你为什么使用特定的内存地址;当涉及系统内存时,它会缓存它能够缓存的所有数据。
  • FWIW 在我的机器上,时间几乎相同。 (1%以内)
  • 我怀疑我们在这里看到的是对内存中保存的变量和寄存器中的变量进行操作的区别。我希望方法 1 将 a 保留在寄存器中,而方法 2 则不然。

标签: c# .net performance field


【解决方案1】:

在我的机器上运行它,我得到了相似的时间差异,但是查看 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”。

【讨论】:

    【解决方案2】:

    如果 1 a 明确存储在寄存器中。其他任何东西都会是一个可怕的编译结果。

    可能 .NET JIT 不愿意/不能将商店转换为 A 以在情况 2 中注册商店。

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

    看到 .NET JIT 执行少量优化并不奇怪。 Stack Overflow 上performance 标签的关注者都知道这一点。

    根据经验,我知道 JIT 更有可能在寄存器中缓存内存负载。这就是案例 1(显然)在每次迭代时都不会访问 B 的原因。

    寄存器计算比内存访问更便宜。如果有问题的内存在 CPU L1 缓存中,情况也是如此(这里就是这种情况)。

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

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

    【讨论】:

    • 最后一部分让我好奇; JIT编译后是否有字段访问之类的东西?如果没有,那么访问字段 A.B.C.D 会和访问 A 一样快吗?
    • 如果是引用类型,则D的地址只能通过A.B、B.C和B.D导航后计算。这很昂贵,因为它会阻塞管道。
    • 如果所有这些类型都是值类型,则 D 在 A 中的偏移量是静态已知的,并且访问 D 的速度与任何其他字段一样快。
    • @usr:它应该基本上一样快,前提是没有一个字段被声明为readonly。除非自从我上次检查以来情况发生了变化,否则最外面的字段为 readonly 将导致 var x=A.B.C.D 被评估为 var temp1=A; var temp2=temp1.B; var temp3=temp2.C; var x=temp3.D;。如果A 很大,那可能会非常昂贵。 (是的,我知道 MS 建议不要使用大型结构,但它们可能非常有效预期)。
    • @supercat 假设它是 A...D++。这是通过获取指向 D 的托管指针并将其写入(就地)来编译的。我假设读取是以相同的方式完成的。这仅适用于左值(字段、数组元素)。对于方法,你说的是真的。
    【解决方案3】:

    method2 : field is read ~100x and set ~100x too = 200x larg_0 (this) + 100x ldfld (load field) + 100x stfld (set field) + 100x ldloc (local)

    method1 : 字段被读取 100 倍但未设置 相当于 method1 减去 100x ldarg_0 (this)

    【讨论】:

      猜你喜欢
      • 2013-01-27
      • 1970-01-01
      • 2011-11-18
      • 2016-08-08
      • 1970-01-01
      • 2014-09-09
      • 2015-02-20
      • 2022-01-22
      • 1970-01-01
      相关资源
      最近更新 更多