performance - 是否有任何现代 CPU 的缓存字节存储实际上比字存储慢?

标签 performance x86 arm cpu-architecture cpu-cache

这是一个 common claim将字节存储到缓存中可能会导致内部读取-修改-写入周期,或者以其他方式损害吞吐量或延迟,而不是存储完整的寄存器。

但我从未见过任何例子。没有 x86 CPU 是这样的,我认为所有高性能 CPU 也可以直接修改缓存行中的任何字节。如果某些微 Controller 或低端 CPU 有缓存,它们是否有所不同?

( 我不计算字可寻址机器 ,或字节可寻址但缺少字节加载/存储指令的 Alpha 。我指的是 ISA 本身支持的最窄存储指令。)

在我的研究中回答 Can modern x86 hardware not store a single byte to memory? ,我发现 Alpha AXP 省略字节存储的原因假定它们将作为真正的字节存储实现到缓存中,而不是包含单词的 RMW 更新。 (因此它会使 L1d 缓存的 ECC 保护更加昂贵,因为它需要字节粒度而不是 32 位)。

我假设在提交到 L1d 缓存期间的 word-RMW 不被视为其他最近实现字节存储的 ISA 的实现选项。

所有现代架构(除了早期的 Alpha)都可以对不可缓存的 MMIO 区域(不是 RMW 周期)进行真正的字节加载/存储,这是为具有相邻字节 I/O 寄存器的设备编写设备驱动程序所必需的。 (例如,使用外部启用/禁用信号来指定更宽总线的哪些部分保存真实数据,例如 this ColdFire CPU/microcontroller 上的 2 位 TSIZ(传输大小),或 PCI/PCIe 单字节传输,或 DDR SDRAM 控制屏蔽所选字节的信号。)

也许在缓存中为字节存储执行 RMW 循环是微 Controller 设计需要考虑的事情,即使它不是针对像 Alpha 这样的 SMP 服务器/工作站的高端超标量流水线设计?

我认为这种说法可能来自字寻址机器。或者来自需要在许多 CPU 上进行多次访问的未对齐的 32 位存储,人们错误地将其概括为字节存储。

为了清楚起见,我希望到相同地址的字节存储循环将在每次迭代中以与字存储循环相同的周期运行。因此,对于填充数组,32 位存储可以比 8 位存储快 4 倍。 (如果 32 位存储使内存带宽饱和,而 8 位存储不饱和,则可能更少。)但除非字节存储有额外的损失,否则您不会获得超过 4 倍的速度差异。 (或任何字宽)。

我说的是 asm。一个好的编译器会在 C 中自动向量化一个字节或 int 存储循环,并使用更广泛的存储或目标 ISA 上的最佳存储,如果它们是连续的。

(并且存储缓冲区中的存储合并也可能导致更广泛地提交到 L1d 缓存以获取连续的字节存储指令,因此这是微基准测试时需要注意的另一件事)

; x86-64 NASM syntax
mov   rdi, rsp
; RDI holds at a 32-bit aligned address
mov   ecx, 1000000000
.loop:                      ; do {
    mov   byte [rdi], al
    mov   byte [rdi+2], dl     ; store two bytes in the same dword
      ; no pointer increment, this is the same 32-bit dword every time
    dec   ecx
    jnz   .loop             ; }while(--ecx != 0}


    mov   eax,60
    xor   edi,edi
    syscall         ; x86-64 Linux sys_exit(0)

或者像这样在 8kiB 数组上循环,每 8 个字节存储 1 个字节或 1 个字(对于 sizeof(unsigned int)=4 和 CHAR_BIT=8 的 C 实现,8kiB,但应该编译为任何类似的函数) C 实现,如果 sizeof(unsigned int) 不是 2 的幂,则只有很小的偏差)。 ASM on Godbolt for a few different ISAs ,要么不展开,要么两个版本的展开量相同。
// volatile defeats auto-vectorization
void byte_stores(volatile unsigned char *arr) {
    for (int outer=0 ; outer<1000 ; outer++)
        for (int i=0 ; i< 1024 ; i++)      // loop over 4k * 2*sizeof(int) chars
            arr[i*2*sizeof(unsigned) + 1] = 123;    // touch one byte of every 2 words
}

// volatile to defeat auto-vectorization: x86 could use AVX2 vpmaskmovd
void word_stores(volatile unsigned int *arr) {
    for (int outer=0 ; outer<1000 ; outer++)
        for (int i=0 ; i<(1024 / sizeof(unsigned)) ; i++)  // same number of chars
            arr[i*2 + 0] = 123;       // touch every other int
}

根据需要调整大小,如果有人可以指向一个系统,我真的很好奇 word_store()byte_store() 快.
(如果实际进行基准测试,请注意动态时钟速度等预热效果,以及触发 TLB 未命中和缓存未命中的第一遍。)

或者,如果用于古老平台的实际 C 编译器不存在或生成不会对存储吞吐量造成瓶颈的次优代码,那么任何手工制作的 asm 都会显示效果。

任何其他证明字节存储速度减慢的方法都可以,我不坚持在数组上进行跨步循环或在一个单词内进行垃圾邮件写入。

我也很乐意提供有关 CPU 内部结构的详细文档 ,或不同指令的 CPU 周期计时数。不过,我对可能基于此声明而未经测试的优化建议或指南持怀疑态度。
  • 任何仍然相关的 CPU 或微 Controller 缓存字节存储有额外的惩罚?
  • 任何仍然相关的 CPU 或微 Controller ,其中不可缓存的字节存储有额外的惩罚?
  • 任何不相关的历史 CPU(带或不带回写或直写缓存),其中上述任一情况为真?最近的例子是什么?

  • 例如ARM Cortex-A 是这种情况吗??还是皮质-M?任何较旧的 ARM 微架构?任何 MIPS 微 Controller 或早期的 MIPS 服务器/工作站 CPU?任何其他随机 RISC(如 PA-RISC)或 CISC(如 VAX 或 486)? (CDC6600 是可字寻址的。)

    或者构建一个涉及负载和存储的测试用例,例如显示来自字节存储的 word-RMW 与负载吞吐量竞争。

    (我不感兴趣表明从字节存储到字加载的存储转发比 word->word 慢,因为 SF 仅在加载完全包含在最近的存储中以接触任何一个时才有效工作是正常的相关字节。但是一些显示字节->字节转发效率低于字->字 SF 的东西会很有趣,也许字节不以字边界开始。)

    ( 我没有提到字节加载,因为这通常很容易t 阅读包含的单词。)

    在像 MIPS 这样的加载/存储架构上,处理字节数据意味着您使用 lblbu加载并归零或符号扩展它,然后将其存储回 sb . (如果您需要在寄存器中的步骤之间截断为 8 位,那么您可能需要额外的指令,因此本地变量通常应该是寄存器大小。除非您希望编译器使用具有 8 位元素的 SIMD 自动矢量化,否则通常 uint8_t本地人很好......)但无论如何,如果你做得对并且你的编译器很好,那么拥有字节数组不应该花费任何额外的指令。

    我注意到 gcc 有 sizeof(uint_fast8_t) == 1在 ARM、AArch64、x86 和 MIPS 上。但是 IDK 我们可以投入多少库存。 x86-64 System V ABI 定义了 uint_fast32_t作为 x86-64 上的 64 位类型。如果他们打算这样做(而不是 32 位,这是 x86-64 的默认操作数大小),uint_fast8_t也应该是 64 位类型。也许在用作数组索引时避免零扩展?如果它在寄存器中作为函数 arg 传递,因为如果您无论如何必须从内存中加载它,它可以免费进行零扩展。

    最佳答案

    我的猜测是错误的。现代 x86 微体系结构在这方面确实与某些(大多数?)其他 ISA 不同。

    即使在高性能非 x86 CPU 上,缓存的窄存储也会受到惩罚。 缓存占用空间的减少仍然可以使 int8_t不过值得使用的数组。 (在某些 ISA 上,例如 MIPS,不需要为寻址模式缩放索引会有所帮助)。

    在实际提交到 L1d 之前,将字节存储指令之间的存储缓冲区中的存储缓冲区合并/合并到同一字也可以减少或消除惩罚。 (x86 有时不能做那么多,因为它的强内存模型要求所有存储都按程序顺序提交。)

    ARM's documentation for Cortex-A15 MPCore (从 2012 年开始)说它在 L1d 中使用 32 位 ECC 粒度,并且实际上确实为窄存储做了一个 word-RMW 来更新数据。

    The L1 data cache supports optional single bit correct and double bit detect error correction logic in both the tag and data arrays. The ECC granularity for the tag array is the tag for a single cache line and the ECC granularity for the data array is a 32-bit word.

    Because of the ECC granularity in the data array, a write to the array cannot update a portion of a 4-byte aligned memory location because there is not enough information to calculate the new ECC value. This is the case for any store instruction that does not write one or more aligned 4-byte regions of memory. In this case, the L1 data memory system reads the existing data in the cache, merges in the modified bytes, and calculates the ECC from the merged value. The L1 memory system attempts to merge multiple stores together to meet the aligned 4-byte ECC granularity and to avoid the read-modify-write requirement.



    (当他们说“L1 内存系统”时,我认为他们指的是存储缓冲区,如果您有尚未提交给 L1d 的连续字节存储。)

    请注意,RMW 是原子的,并且只涉及被修改的独占缓存行。这是一个不影响内存模型的实现细节。 所以我对 Can modern x86 hardware not store a single byte to memory? 的结论x86 仍然(可能)是正确的,提供字节存储指令的所有其他 ISA 也是如此。

    Cortex-A15 MPCore是一个 3 路乱序执行 CPU,所以它不是最小功率/简单的 ARM 设计,但他们选择在 OoO exec 上使用晶体管而不是高效的字节存储。

    大概不需要支持高效的未对齐存储(x86 软件更可能假设/利用),较慢的字节存储被认为是值得的,因为 L1d 的 ECC 可靠性更高,而没有过多的开销。

    Cortex-A15 可能不是唯一的,也不是最新的以这种方式工作的 ARM 内核。

    其他示例(由@HadiBrais 在评论中找到):
  • 阿尔法 21264 (请参阅 this doc 第 8 章的表 8-1)其 L1d 缓存具有 8 字节 ECC 粒度。较窄的存储(包括 32 位)在提交到 L1d 时会导致 RMW,如果它们没有先合并到存储缓冲区中。该文档解释了 L1d 每个时钟可以做什么的全部细节。并且特别记录了存储缓冲区确实合并了存储。
  • PowerPC RS64-II 和 RS64-III (参见 this 文档中的错误部分)。根据 this abstract ,RS/6000处理器的L1每32位数据有7位ECC。

  • Alpha 从一开始就积极地采用 64 位,因此 8 字节粒度是有道理的,尤其是当 RMW 成本大部分可以被存储缓冲区隐藏/吸收时。 (例如,对于该 CPU 上的大多数代码,正常瓶颈可能在别处;它的多端口缓存通常每个时钟可以处理 2 个操作。)

    POWER/PowerPC64 从 32 位 PowerPC 发展而来,并且可能关心运行具有 32 位整数和指针的 32 位代码。 (因此更有可能对无法合并的数据结构进行非连续 32 位存储。)因此 32 位 ECC 粒度在那里很有意义。

    关于performance - 是否有任何现代 CPU 的缓存字节存储实际上比字存储慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54217528/

    相关文章:

    c# - 在 C# 中创建一个随机文件

    c - 我们如何为变量指定物理地址?

    gcc - 修改gcc以容纳更多寄存器

    assembly - 'push imm'如何编码?

    Javascript 字符串连接比这个例子更快?

    performance - 为什么在 F# 中使用引用大值的字段创建记录如此缓慢?

    x86 - SSE:将短整数转换为 float

    android - 如何在 Android 中构建没有 JNI 的共享库?

    linux - 无法让 Rebol3 在 Linux-ARMhf 上运行

    c - 嵌入式系统负载及性能测试