当前位置: 首页 > 知识库问答 >
问题:

在现代CPU中,缓存的字节存储实际上比字存储慢吗?

陈飞
2023-03-14

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

在我的研究中,当回答现代x86硬件不能将一个字节存储到内存中吗?时,我发现Alpha AXP省略字节存储的原因是假设它们将被实现为真正的字节存储到缓存中,而不是包含字的RMW更新。(因此,它会使L1d缓存的ECC保护更加昂贵,因为它需要字节粒度而不是32位)。

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

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

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

为了明确起见,我预计到相同地址的字节存储循环将与字存储循环在每个迭代中以相同的周期运行。因此,对于填充数组,32位存储可以比8位存储快4倍。(如果32位存储会使内存带宽饱和,可能会更少,但8位存储不会。)但是除非字节存储有额外的惩罚,否则你不会得到超过4倍的速度差异。(或者不管字宽是多少)。

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

; 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)
// 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
}

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

任何其他方式来演示字节存储的放缓都很好,我不坚持在数组上进行跨行循环或在一个字内进行垃圾邮件写入。

我也可以提供关于CPU内部的详细文档,或者不同指令的CPU周期定时数。不过,我对基于这个声明的优化建议或指南持怀疑态度,这些建议或指南可能没有经过测试。

    null

或者构造一个包含负载和存储的测试用例,例如,从与负载吞吐量竞争的字节存储中显示word-RMW。

(我并不想说明从字节存储到字加载的存储转发比Word->word慢,因为通常只有当加载完全包含在最近的存储中并触及任何相关字节时,SF才有效。但如果显示Byte->byte转发不如Word->word SF效率低,可能是因为字节不是从字边界开始的。)

(我没有提到字节加载,因为这通常很容易:从缓存或RAM中访问完整的字,然后提取您想要的字节。除了MMIO之外,实现细节很难区分,在MMIO中,CPU肯定不会读取包含的字。)

在像MIPS这样的加载/存储体系结构中,使用字节数据仅仅意味着使用LBLBU加载和归零或签名扩展它,然后用SB存储它。(如果您需要在寄存器中的步骤之间截断到8位,那么您可能需要额外的指令,所以本地变量通常应该是寄存器大小的。除非您希望编译器使用带有8位元素的SIMD自动向量化,那么通常uint8_t本地变量是好的…)但是无论如何,如果你做得对,你的编译器很好,它应该不会花费任何额外的指令来拥有字节数组。

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

共有1个答案

谭曦
2023-03-14

我猜错了。现代x86微架构在这方面确实与某些(大多数)不同其他ISA。

即使在高性能的非x86 CPU上,缓存的窄存储区也会有损失。但是,缓存占用的减少仍然可以使int8_t数组值得使用。(在一些ISA(如MIPS)上,不需要为寻址模式缩放索引是有帮助的)。

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

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

请注意,RMW是原子的,并且只涉及被修改的独占缓存行。这是一个不影响内存模型的实现细节。那么我的结论是现代x86硬件可以不存储一个字节到内存吗?仍然(可能)正确的是,x86可以,其他提供字节存储指令的ISA也可以。

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

>

  • Alpha 21264(见本文件第8章表8-1)的L1d缓存具有8字节的ECC粒度。较窄的存储(包括32位)在提交到L1d时会导致RMW(如果它们没有首先在存储缓冲区中合并)。doc解释了L1d每个时钟可以做什么的全部细节。特别是记录存储缓冲区合并存储的情况。

    PowerPC RS64-II和RS64-III(请参阅本文档中有关错误的部分)。根据这个摘要,RS/6000处理器的L1对于每32位数据有7位ECC。

    Alpha从头到尾都是64位的,所以8字节的粒度是有意义的,特别是如果RMW成本可以被存储缓冲区隐藏/吸收。(例如,对于该CPU上的大多数代码来说,正常的瓶颈可能在其他地方;它的多端口缓存通常可以在每个时钟处理2个操作。)

  •  类似资料:
    • 我有一个项目,使用泰勒级数创建一个科学计算器。此外,我正在分析IEEE 754标准浮点系统的数字。 在我的计算器中,用户选择要单精度还是双精度:我使用浮点和双变量,然后按照ieee 754规范分析数字。 如果用户想要双精度,分析如下: 这给了我 > 这个数字是以二进制格式存储在pc内存中的实际数字吗? 如果不是,我有没有办法得到存储的数字的真实值? 还有,为什么会发生这种情况? 它只打印< cod

    • 我试图理解DirectByteBuffer如何在Linux上工作,并编写了以下在strace下运行的非常简单的程序: 实际上,我期望一些mmap或sys\u brk系统调用直接从操作系统分配内存,但实际上它只是设置请求页面的读写保护。我的意思是: 这似乎是分配直接缓冲区比分配堆缓冲区慢的原因,因为每次分配都需要系统调用。 如果我错了,请纠正我,但是堆缓冲区分配(如果发生在TLAB内部)相当于返回一

    • 我想为Spring Cache执行以下操作。 > 检查传递的字符串是否存在于缓存中。如果存在,则返回true,如果不存在,则添加到缓存;checkInCache(字符串str) 从缓存逐出字符串(String str) 尝试如下 @组件公共类FlightCache{ 并在配置类上添加了@EnableCaching注释。 错误:

    • 问题内容: 我有一个用Java创建的字节数组。它代表某些文件的内容。我不知道这个数组的最大大小。它可以是不同的大小。我想将其存储在mysql中。我应该在mysql中使用哪种类型? 问题答案: 使用,, Mysql为列选择正确的类型

    • 我使用什么数据类型在协议缓冲区消息中存储单个字节?看到https://developers.google.com/protocol-buffers/docs/proto#scalar的列表,似乎*int32类型之一最合适。有没有更有效的方法来存储单个字节?

    • 问题内容: 假设我有两个陈述。 哪个是堆栈内存,哪个存储在堆中? 两者之间有什么区别? 创建了多少个对象,内存中的引用如何? 最佳做法是什么? 问题答案: 所有对象都存储在堆中(包括其字段的值)。1个 局部变量(包括参数)始终包含原始值或引用,并存储在堆栈中。1个 因此,对于您的两行: 您将在堆上有两个对象(两个包含的String对象)和两个引用(每个对象一个)在堆栈上(提供且是局部变量)。 (实