c – 现代x86硬件不能将单个字节存储到内存中吗?
作者:互联网
说到C的并发内存模型,Stroustrup的C编程语言,第4版,第1节. 41.2.1,说:
… (like most modern hardware) the machine could not load or store anything smaller than a word.
但是,我的x86处理器,几年前,可以存储小于一个字的对象.例如:
#include <iostream>
int main()
{
char a = 5;
char b = 25;
a = b;
std::cout << int(a) << "\n";
return 0;
}
如果没有优化,GCC将其编译为:
[...]
movb $5, -1(%rbp) # a = 5, one byte
movb $25, -2(%rbp) # b = 25, one byte
movzbl -2(%rbp), %eax # load b, one byte, not extending the sign
movb %al, -1(%rbp) # a = b, one byte
[...]
评论是由我提出的,但是汇编是由GCC提出的.当然,它运行良好.
显然,我不明白Stroustrup在谈到硬件可以加载和存储任何小于一个单词的内容时所说的内容.据我所知,我的程序除了加载和存储小于单词的对象外什么都不做.
C对零成本,硬件友好的抽象的彻底关注使C与其他易于掌握的编程语言区别开来.因此,如果Stroustrup在公交车上有一个有趣的信号心理模型,或者有其他类似的东西,那么我想了解Stroustrup的模型.
什么是Stroustrup谈论,拜托?
更长时间的背景声明
这是Stroustrup在更全面的背景下的引用:
Consider what might happen if a linker allocated [variables of
char
type like]c
andb
in the same word in memory and (like most modern hardware) the machine could not load or store anything smaller than a word…. Without a well-defined and reasonable memory model, thread 1 might read the word containingb
andc
, changec
, and write the word back into memory. At the same time, thread 2 could do the same withb
. Then, whichever thread managed to read the word first and whichever thread managed to write its result back into memory last would determine the result….
补充说明
我不相信Stroustrup在讨论缓存行.即使他是,据我所知,缓存一致性协议将透明地处理该问题,除非在硬件I / O期间.
我检查了处理器的硬件数据表.电子方面,我的处理器(Intel Ivy Bridge)似乎通过某种16位多路复用方案来解决DDR3L内存问题,所以我不知道那是什么意思.我不清楚这与Stroustrup的观点有很大关系.
Stroustrup是一个聪明的人,也是一位杰出的科学家,所以我不怀疑他正在采取一些明智的做法.我很迷惑.
另请参阅this question.我的问题在几个方面类似于链接的问题,并且链接问题的答案在这里也很有用.然而,我的问题还在于硬件/总线模型,它促使C成为它的方式,并导致Stroustrup写出他写的东西.我不仅仅针对C标准正式保证的答案寻求答案,而且还希望理解为什么C标准会保证它.什么是潜在的想法?这也是我的问题的一部分.
解决方法:
TL:DR:在每个具有字节存储指令(包括x86)的现代ISA上,它们都是原子的,不会干扰周围的字节. (我不知道任何旧的ISA,其中字节存储指令可以“发明写入”到相邻字节.)
实际的实现机制(in non-x86 CPUs)有时是一个内部RMW循环来修改高速缓存行中的整个单词,但是它在内核中“无形地”完成,而它拥有高速缓存行的独占所有权,所以它只是一个性能问题,而不是正确性. (并且在存储缓冲区中合并有时可以将字节存储指令转换为对L1d缓存的有效全字提交.)
关于Stroustrup的措辞
我认为这不是一个非常准确,清晰或有用的陈述.更准确地说,现代CPU无法加载或存储小于缓存行的任何内容. (虽然对于不可缓存的内存区域不是这样,例如对于MMIO.)
做一个假设的例子谈论内存模型可能会更好,而不是暗示真正的硬件是这样的.但是,如果我们尝试,我们可能会找到一种不那么明显或完全错误的解释,这可能是Stroustrup在撰写此内容以介绍内存模型主题时的想法. (对不起,这个答案太长了;我最后写了很多东西,同时猜测他的意思和相关主题……)
或者这可能是高级语言设计者不是硬件专家,或者至少偶尔会做出错误陈述的另一种情况.
我认为Stroustrup正在讨论CPU如何在内部工作以实现字节存储指令.他建议没有定义明确且合理的内存模型的CPU可能会在高速缓存行中使用包含单词的非原子RMW实现字节存储,或者在没有高速缓存的CPU的内存中实现.
对于高性能x86 CPU,即使是关于内部(非外部可见)行为的这种较弱的声明也是如此.现代Intel CPU对字节存储没有吞吐量损失,甚至没有跨越缓存线边界的未对齐字或向量存储. AMD类似.
如果存储器提交到L1D高速缓存,则字节或未对齐存储必须执行RMW周期,它将以我们可以使用性能计数器测量的方式干扰存储和/或加载指令/ uop吞吐量. (在精心设计的实验中,在提交到L1d缓存隐藏成本之前,避免了存储缓冲区中存储合并的可能性,因为存储执行单元在当前CPU上每个时钟只能运行1个存储.)
但是,非x86 ISA的一些高性能设计确实使用原子RMW周期在内部将存储提交到L1d缓存. Are there any modern CPUs where a cached byte store is actually slower than a word store?缓存行整个时间都处于MESI Exclusive / Modified状态,因此无法引入任何正确性问题,只有很小的性能损失.这与可以从其他CPU执行商店的操作非常不同. (以下关于未发生的事情的论点仍然适用,但我的更新可能遗漏了一些仍然认为原子缓存-RWW不太可能的东西.)
(在许多非x86 ISA上,根本不支持未对齐的存储,或者比x86软件更少使用.而弱排序的ISA允许在存储缓冲区中进行更多的合并,因此没有多少字节存储指令实际上导致单个存储缓冲区字节提交到L1d.如果没有这些花哨的(耗电的)高速缓存访问硬件的动机,在一些设计中,散乱字节存储的字RMW是可接受的权衡.)
Alpha AXP是1992年的高性能RISC设计,着名(并且在现代非DSP ISA中独一无二)省略了字节加载/存储指令,直到Alpha 21164A (EV56) in 1996.显然,他们并不认为word-RMW是实现字节存储的可行选择,因为对于仅实现32位和64位对齐存储,所引用的优点之一是用于L1D高速缓存的更有效的ECC. “Traditional SECDED ECC would require 7 extra bits over 32-bit granules (22% overhead) versus 4 extra bits over 8-bit granules (50% overhead).”(@Paul A. Clayton关于字与字节寻址的答案还有一些其他有趣的计算机架构.)如果使用word-RMW实现字节存储,你仍然可以使用字粒度进行错误检测/纠正.
由于这个原因,当前的Intel CPU仅在L1D中使用奇偶校验(不是ECC).请参阅this Q&A关于硬件(不)消除“静默存储”:在写入之前检查缓存的旧内容,以避免标记线脏,如果匹配则需要RMW而不仅仅是存储,这是一个主要障碍.
事实证明,一些高性能流水线设计确实使用原子字RMW来提交L1d,尽管它使存储器管道停滞不前,但(正如我在下面所说),任何外部可见的RMW到RAM的可能性要小得多.
Word-RMW对于MMIO字节存储也不是一个有用的选项,所以除非你的架构不需要IO的子字存储,否则你需要对IO进行某种特殊处理(如Alpha’s sparse I/O space中的字加载/ stores被映射到字节加载/存储,因此它可以使用商用PCI卡而不需要没有字节IO寄存器的特殊硬件).
作为@Margaret points out,DDR3存储器控制器可以通过设置屏蔽突发的其他字节的控制信号来进行字节存储.将此信息提供给内存控制器(对于未缓存的存储)的相同机制也可以将该信息与加载或存储一起传递到MMIO空间.所以有真正做的硬件机制
甚至在面向突发的存储器系统上也是一个字节存储器,并且现代CPU很可能会使用它而不是实现RMW,因为它可能更简单并且对于MMIO的正确性要好得多.
How many and what size cycles will be needed to perform longword transferred to the CPU显示了ColdFire微控制器如何通过外部信号线发出传输大小(字节/字/长字/ 16字节线)的信号,即使32位宽的存储器连接到32位,也可以进行字节加载/存储数据总线.像这样的东西大概是大多数内存总线设置的典型(但我不知道). ColdFire示例很复杂,还可以配置为使用16位或8位内存,为更广泛的传输采用额外的周期.但是没关系,重要的是它有传输大小的外部信号,告诉内存HW它实际写入哪个字节.
Stroustrup的next paragraph是
“The C++ memory model guarantees that 070010. This is exactly what we would naively expect. It is the compiler’s job to protect us from the sometimes very strange and subtle behaviors of modern hardware. How a compiler and hardware combination achieves that is up to the compiler. …”
显然他认为真正的现代硬件可能无法提供“安全”的字节加载/存储.设计硬件内存模型的人与C/C++人员一致,并意识到如果字节存储指令可以踩到相邻字节,那么它们对程序员/编译器就没有用.
除早期Alpha AXP之外的所有现代(非DSP)架构都具有字节存储和加载指令,而AFAIK这些架构在结构上都定义为不影响相邻字节.然而,他们在硬件中实现了这一点,软件不需要关心正确性.即使MIPS的第一个版本(1983年)也有字节和半字加载/存储,它是一个非常注重字的ISA.
但是,他实际上并没有声称大多数现代硬件需要任何特殊的编译器支持来实现C内存模型的这一部分,只是有些人可能会这样做.也许他真的只是讨论第二段中的字可寻址DSP(其中C和C实现经常使用16或32位字符,正如Stroustrup所讨论的那种编译器工作方式.)
大多数“现代”CPU(包括所有x86)都有一个L1D缓存.它们将获取整个缓存行(通常为64个字节)并在每个缓存行的基础上跟踪脏/非脏.因此,如果它们都在同一个高速缓存行中,则两个相邻的字节与两个相邻的字几乎完全相同.写入一个字节或字将导致整行的读取,并最终写回整行.请参阅Ulrich Drepper的What Every Programmer Should Know About Memory.你是正确的MESI(或像MESIF / MOESI这样的派生物)确保这不是问题. (但同样,这是因为硬件实现了一个理智的内存模型.)
存储只能在线路处于修改状态(MESI)时提交到L1D缓存.因此,即使内部硬件实现对于字节来说很慢并且需要额外的时间将字节合并到高速缓存行中的包含字中,它实际上是原子读取修改写入,只要它不允许该行无效并且重新开始. – 读取和写入之间的获取. (While this cache has the line in Modified state, no other cache can have a valid copy).请参见@old_timer’s comment,以获得相同的要点(但也适用于内存控制器中的RMW).
这比例如原子xchg或从寄存器添加也需要ALU和寄存器访问,因为所涉及的所有HW都在同一个流水线阶段,这可以简单地停止一两个额外的循环.这显然对性能有害,并需要额外的硬件才能让管道阶段发出信号表明它正在停止运行.这并不一定与Stroustrup的第一个主张相冲突,因为他在谈论的是没有记忆模型的假设ISA,但它仍然是一个延伸.
在单核微控制器上,用于缓存字节存储的内部字RMW将更加合理,因为在原子RMW缓存字更新期间不会有来自其他内核的无效请求,它们必须延迟响应.但这对于无法缓存的区域的I / O没有帮助.我说微控制器,因为其他单核CPU设计通常支持某种多插槽SMP.
许多RISC ISA不支持单指令的未对齐字加载/存储,但这是一个单独的问题(当负载跨越两个缓存行甚至页面时,难以处理这种情况,这不会发生在字节或对齐的情况下半字).然而,越来越多的ISA正在为最近版本中的未对齐加载/存储添加有保障的支持. (例如2014年的MIPS32/64 Release 6,我认为AArch64和最近的32位ARM).
Stroustrup的书于07年出版,当时Alpha已经死了多年.第一版是published in 1985,当时RISC是一个新的大创意(例如1983年的斯坦福MIPS,according to Wikipedia’s timeline of computing HW,但当时的“现代”CPU是字节存储的字节可寻址.Cyber CDC 6600是可以字寻址的,可能还在,但不能称之为现代.
即使非常面向字的RISC机器(如MIPS和SPARC)也具有字节存储和字节加载(带符号或零扩展)指令.它们不支持未对齐的字加载,简化缓存(或者如果没有缓存则可以访问内存)和加载端口,但是您可以使用一条指令加载任何单个字节,更重要的是存储一个没有任何体系结构可见的字节的字节原子重写周围的字节. (虽然缓存商店可以
我认为如果针对没有字节存储的Alpha ISA版本,Alpha上的C 11(在语言中引入了线程感知内存模型)将需要使用32位字符.或者它必须使用带有LL / SC的软件atomic-RMW,因为它无法证明没有其他线程可以有一个指针让它们写入相邻的字节.
IDK在任何CPU中的字节加载/存储指令的速度有多慢,它们在硬件中实现但不像字加载/存储那么便宜.只要使用movzx / movsx来避免部分寄存器错误依赖或合并停顿,x86上的字节加载便宜. On AMD pre-Ryzen, movsx
/movzx
needs an extra ALU uop, but otherwise zero/sign extension is handled right in the load port on Intel and AMD CPUs.)主要的x86缺点是你需要一个单独的加载指令而不是使用内存操作数作为ALU指令的源(如果你要将一个零扩展字节添加到一个32位整数),节省了前端uop吞吐量带宽和代码大小.或者,如果您只是在字节寄存器中添加一个字节,那么x86基本上没有任何缺点.无论如何,RISC加载存储ISA始终需要单独的加载和存储指令. x86字节存储并不比32位存储更昂贵.
作为一个性能问题,具有慢字节存储的硬件的良好C实现可能会将每个字符串放在自己的字中并尽可能使用字加载/存储(例如,对于结构体外的全局变量,以及堆栈上的本地变量). IDK如果MIPS / ARM的任何实际实现/无论是否有慢速字节加载/存储,但如果是这样,gcc可能有-mtune =选项来控制它.
That doesn’t help for char[]
,或者当你不知道它指向何处时取消引用char *. (这包括你用于MMIO的volatile char *.)因此,让编译器链接器将char变量放在单独的单词中并不是一个完整的解决方案,只是在真正的字节存储很慢时才会出现性能问题.
PS:关于Alpha的更多信息:
由于很多原因,Alpha很有趣:为数不多的64位ISA之一,而不是现有32位ISA的扩展.作为最新的清洁版ISA之一,Itanium是几年后的另一个尝试了一些简洁的CPU架构理念.
From 070023.
When the Alpha architecture was introduced, it was unique amongst RISC architectures for eschewing 8-bit and 16-bit loads and stores. It supported 32-bit and 64-bit loads and stores (longword and quadword, in Digital’s nomenclature). The co-architects (Dick Sites, Rich Witek) justified this decision by citing the advantages:
- Byte support in the cache and memory sub-system tends to slow down accesses for 32-bit and 64-bit quantities.
- Byte support makes it hard to build high-speed error-correction circuitry into the cache/memory sub-system.
Alpha compensates by providing powerful instructions for manipulating bytes and byte groups within 64-bit registers. Standard benchmarks for string operations (e.g., some of the Byte benchmarks) show that Alpha performs very well on byte manipulation.
标签:memory-model,c,x86,concurrency,assembly 来源: https://codeday.me/bug/20190915/1806229.html