这是一个common claim,缓存中的字节存储可能导致内部读 - 修改 - 写周期,或者与存储完整寄存器相比会损害吞吐量或延迟。
但我从未见过任何例子。没有x86 CPU是这样的,我认为所有高性能CPU也可以直接修改缓存行中的任何字节。一些微控制器或低端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循环,即使它不是针对像Alpha这样的SMP服务器/工作站的高端超标量流水线设计?
我认为这种说法可能来自可以用字寻址的机器。或者来自未对齐的32位存储,需要在许多CPU上进行多次访问,并且人们错误地将其从一般存储到字节存储。
为了清楚起见,我希望到同一地址的字节存储循环将在每次迭代中以与字存储循环相同的周期运行。因此,对于填充阵列,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个字(对于一个C实现,sizeof(unsigned int)= 4而CHAR_BIT = 8用于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
arr[i*2 + 0] = 123; // touch every other int
}
根据需要调整大小,如果有人能指出word_store()比byte_store()更快的系统,我真的很好奇。 (如果实际是基准测试,请注意动态时钟速度等热身效应,以及触发TLB未命中和缓存未命中的第一次传递。)
或者,如果不存在古代平台的实际C编译器或生成不会对商店吞吐量造成瓶颈的次优代码,那么任何手工制作的asm都会显示效果。
任何其他证明字节存储速度减慢的方法都很好,我不坚持在数组上进行跨步循环或在一个单词中发送垃圾邮件。
关于CPU内部的详细文档或不同指令的CPU周期时序数,我也可以。不过,我对未经测试的可能基于此声明的优化建议或指南持怀疑态度。
任何仍然相关的CPU或微控制器,其中缓存的字节存储有额外的惩罚?
任何仍然相关的CPU或微控制器,其中不可缓存的字节存储会有额外的惩罚?
任何不相关的历史CPU(有或没有回写或直写高速缓存),其中任何一个都是真的?最近的例子是什么?
例如这是ARM Cortex-A的情况吗?还是Cortex-M?任何旧的ARM微体系结构?任何MIPS微控制器或早期的MIPS服务器/工作站CPU?任何其他随机RISC如PA-RISC,或像VAX或486这样的CISC? (CDC6600可以进行单词寻址。)
或者构建一个涉及负载和存储的测试用例,例如:显示来自字节存储的word-RMW与负载吞吐量竞争。
(我对显示从字节存储到字加载的存储转发比字 - >字更慢感兴趣,因为正常情况下,当负载完全包含在最近的存储中以触摸任何一个时,SF才能正常工作。相关的字节。但是显示字节 - >字节转发效率低于字 - >字SF的东西会很有趣,可能是字节不是从字边界开始的。)
(我没有提到字节加载,因为这通常很简单:从缓存或RAM中访问一个完整的字然后提取你想要的字节。除了MMIO之外,这个实现细节是无法区分的,其中CPU肯定不会读取包含的字。 )
在像MIPS这样的加载/存储架构上,使用字节数据只意味着你使用lb或lbu加载并对其进行零或符号扩展,然后使用sb将其存储回来。 (如果你需要在寄存器中的步骤之间截断8位,那么你可能需要一个额外的指令,因此本地变量通常应该是寄存器大小。除非你希望编译器使用8位元素的SIMD自动向量化,然后经常uint8_t本地人很好......)但无论如何,如果你做得对,你的编译器是好的,它不应该花费任何额外的指令来拥有字节数组。
我注意到gcc在ARM,AArch64,x86和MIPS上有sizeof(uint_fast8_t) == 1。但IDK我们可以投入多少库存。 x86-64 System V ABI在x86-64上将uint_fast32_t定义为64位类型。如果他们要这样做(而不是32位,这是x86-64的默认操作数大小),uint_fast8_t也应该是64位类型。当用作数组索引时,可能避免零扩展?如果它作为函数arg在寄存器中传递,因为如果你不得不从内存中加载它,它可以免费零扩展。
答案
我猜是错的。现代x86微体系结构在某种程度上与一些(大多数?)其他ISA非常不同。
即使在高性能的非x86 CPU上,缓存的窄存储也会受到惩罚。尽管如此,缓存占用空间的减少仍然可以使int8_t阵列值得使用。 (对于像MIPS这样的一些ISA,不需要为寻址模式扩展索引有帮助)。
在字节之间将存储缓冲区中的合并/合并在实际提交到L1d之前将指令存储到相同的字也可以减少或消除惩罚。 (x86有时不能做到这一点,因为它强大的内存模型要求所有商店按程序顺序提交。)
ARM's documentation for Cortex-A15 MPCore(来自~2012)表示它在L1d中使用32位ECC粒度,事实上确实为窄存储做了一个字RMW来更新数据。
L1数据高速缓存支持标签和数据阵列中的可选单位校正和双位检测纠错逻辑。标签阵列的ECC粒度是单个高速缓存行的标记,数据阵列的ECC粒度是32位字。
由于数据阵列中的ECC粒度,对数组的写入不能更新4字节对齐的存储器位置的一部分,因为没有足够的信息来计算新的ECC值。对于没有写入一个或多个对齐的4字节存储区域的任何存储指令就是这种情况。在这种情况下,L1数据存储器系统读取高速缓存中的现有数据,合并修改的字节,并根据合并的值计算ECC。 L1存储器系统尝试将多个存储器合并在一起以满足对齐的4字节ECC粒度并避免读取 - 修改 - 写入要求。
(当他们说“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在评论中找到):
Alpha 21264(参见this doc第8章表8-1)的L1d缓存具有8字节ECC粒度。较窄的存储(包括32位)在它们提交到L1d时会产生RMW,如果它们没有首先在存储缓冲区中合并。该文档解释了每个时钟L1d可以做什么的完整细节。特别是商店缓冲区合并商店的文档。
PowerPC RS64-II和RS64-III(参见this doc中的错误部分)。根据this abstract的说法,RS / 6000处理器的L1为每32位数据提供7位ECC。
Alpha从头开始是积极的64位,因此8字节粒度有一定意义,特别是如果RMW成本大部分可以被存储缓冲区隐藏/吸收。 (例如,对于该CPU上的大多数代码而言,正常的瓶颈可能在其他地方;其多端口缓存通常可以在每个时钟处理2个操作。)
POWER / PowerPC64源于32位PowerPC,可能关心运行32位代码和32位整数和指针。 (因此更有可能对无法合并的数据结构执行非连续的32位存储。)因此,32位ECC粒度在那里很有意义。
另一答案
cortex-m7 trm,手册的缓存ram部分。
在无差错系统中,主要的性能影响是数据端非完整存储的读 - 修改 - 写方案的成本。如果存储缓冲器槽不包含至少一个完整的32位字,则它必须读取该字以便能够计算校验位。这可能是因为软件仅使用字节或半字存储指令写入存储器区域。然后可以将数据写入RAM。此附加读取可能会对性能产生负面影响,因为它会阻止插槽用于另一次写入。
.
存储器系统的缓冲和突出功能掩盖了附加读取的一部分,对于大多数代码来说它可以忽略不计。但是,ARM建议您尽可能使用可缓存的STRB和STRH指令来降低性能影响。
我有皮质-m7s,但到目前为止还没有进行过测试来证明这一点。
“读取单词”是什么意思,它是SRAM中一个存储位置的读取,它是数据高速缓存的一部分。它不是一个高级系统内存的东西。
缓存的内部是围绕SRAM块构建的,SRAM块是快速SRAM,它使缓存成为现实,比系统内存更快,快速将答案返回给处理器等。这种读取 - 修改 - 写入(RMW)不是高级写政策的事情。他们所说的是如果有命中并且写策略说要将写保存在高速缓存中,则需要将字节或半字写入这些SRAM中的一个。如本文所示,具有ECC的数据高速缓存数据SRAM的宽度为32 + 7位宽。 32位数据7位ECC校验位。您必须将所有39位保持在一起才能使ECC工作。根据定义,您不能仅修改某些位,因为这会导致ECC错误。
每当存储在数据高速缓存数据SRAM,8,16或32位中的32位字需要改变任何数量的位时,必须重新计算7个校验位并且一次写入所有39位。对于8位或16位,STRB或STRH写入,32位数据需要读取8或16位,修改后该字中的其余数据位不变,计算7个ECC校验位,并将39位写入sram 。
检查位的计算理想地/可能在设置写入的相同时钟周期内,但读取和写入不在同一时钟周期中,因此至少需要两个单独的周期来写入到达高速缓存的数据在一个时钟周期。有些技巧可以延迟写入,有时也可能会造成伤害,但通常会将其移动到一个未使用过的循环中,如果你愿意的话可以使它自由。但它不会与读取时钟周期相同。
他们说如果你抓住你的嘴并设法让足够小的商店足够快地到达缓存,他们将停止处理器,直到它们能够赶上。
该文档还描述了无ECC SRAM为32位宽,这意味着在没有ECC支持的情况下编译内核时也是如此。我无法访问此内存接口的信号或文档,所以我无法肯定地说,但如果它实现为没有字节通道控件的32位宽接口,那么你有同样的问题,它只能写一个完整的32位项目这个SRAM而不是分数所以要改变8或16位你必须RMW,在缓存的内部。
简单回答为什么不使用更窄的内存,芯片的大小,ECC的大小加倍,因为即使宽度越来越小,你可以使用多少检查位的限制(每8位7位是更多比特每32比特保存7比特。内存越窄,你就会有更多的信号路由,并且无法密集地将内存打包。一套公寓和一堆独立的房子可以容纳相同数量的人。前门的道路和人行道而不是走廊。
并且esp使用这样的单核处理器,除非你有意尝试(我会),你不可能不小心碰到这个,为什么要把产品的成本提高到:它可能不会发生?
请注意,即使使用多核处理器,您也会看到这样的内存。
编辑。
好的开始测试了。
0800007c :
800007c: b430 push {r4, r5}
800007e: 6814 ldr r4, [r2, #0]
08000080 :
8000080: 6803 ldr r3, [r0, #0]
8000082: 6803 ldr r3, [r0, #0]
8000084: 6803 ldr r3, [r0, #0]
8000086: 6803 ldr r3, [r0, #0]
8000088: 6803 ldr r3, [r0, #0]
800008a: 6803 ldr r3, [r0, #0]
800008c: 6803 ldr r3, [r0, #0]
800008e: 6803 ldr r3, [r0, #0]
8000090: 6803 ldr r3, [r0, #0]
8000092: 6803 ldr r3, [r0, #0]
8000094: 6803 ldr r3, [r0, #0]
8000096: 6803 ldr r3, [r0, #0]
8000098: 6803 ldr r3, [r0, #0]
800009a: 6803 ldr r3, [r0, #0]
800009c: 6803 ldr r3, [r0, #0]
800009e: 6803 ldr r3, [r0, #0]
80000a0: 3901 subs r1, #1
80000a2: d1ed bne.n 8000080
80000a4: 6815 ldr r5, [r2, #0]
80000a6: 1b60 subs r0, r4, r5
80000a8: bc30 pop {r4, r5}
80000aa: 4770 bx lr
每个都有一个加载字(ldr),加载字节(ldrb),存储字(str)和存储字节(strb)版本,每个都至少在16字节边界上对齐,直到循环地址的顶部。
启用icache和dcache
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0001000B
00010007
0001000B
00010007
0001000C
00010007
0002FFFD
0002FFFD
虽然这些商店的堆积如此,但是一个字节写入比单词写入长3倍。
但如果你没有那么难以点击缓存
0800019c :
800019c: b430 push {r4, r5}
800019e: 6814 ldr r4, [r2, #0]
080001a0 :
80001a0: 7003 strb r3, [r0, #0]
80001a2: 46c0 nop ; (mov r8, r8)
80001a4: 46c0 nop ; (mov r8, r8)
80001a6: 46c0 nop ; (mov r8, r8)
80001a8: 7003 strb r3, [r0, #0]
80001aa: 46c0 nop ; (mov r8, r8)
80001ac: 46c0 nop ; (mov r8, r8)
80001ae: 46c0 nop ; (mov r8, r8)
80001b0: 7003 strb r3, [r0, #0]
80001b2: 46c0 nop ; (mov r8, r8)
80001b4: 46c0 nop ; (mov r8, r8)
80001b6: 46c0 nop ; (mov r8, r8)
80001b8: 7003 strb r3, [r0, #0]
80001ba: 46c0 nop ; (mov r8, r8)
80001bc: 46c0 nop ; (mov r8, r8)
80001be: 46c0 nop ; (mov r8, r8)
80001c0: 3901 subs r1, #1
80001c2: d1ed bne.n 80001a0
80001c4: 6815 ldr r5, [r2, #0]
80001c6: 1b60 subs r0, r4, r5
80001c8: bc30 pop {r4, r5}
80001ca: 4770 bx lr
然后单词和字节占用相同的时间
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0000C00B
0000C007
0000C00B