thb*_*thb 25 c++ concurrency x86 assembly memory-model
说到C++的并发内存模型,Stroustrup的C++编程语言,第4版,第1节.41.2.1,说:
...(像大多数现代硬件一样)机器无法加载或存储任何小于单词的东西.
但是,我的x86处理器,几年前,可以存储小于一个字的对象.例如:
#include <iostream>
int main()
{
char a = 5;
char b = 25;
a = b;
std::cout << int(a) << "\n";
return 0;
}
Run Code Online (Sandbox Code Playgroud)
如果没有优化,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
[...]
Run Code Online (Sandbox Code Playgroud)
评论是由我提出的,但是汇编是由GCC提出的.当然,它运行良好.
显然,我不明白Stroustrup在谈到硬件可以加载和存储任何小于一个单词的内容时所说的内容.据我所知,我的计划什么也不做,但加载和存储对象小于一个字的.
C++对零成本,硬件友好的抽象的彻底关注使C++与其他易于掌握的编程语言区别开来.因此,如果Stroustrup在公交车上有一个有趣的信号心理模型,或者有其他类似的东西,那么我想了解Stroustrup的模型.
什么是 Stroustrup谈论,拜托?
更长时间的背景声明
这是Stroustrup在更全面的背景下的引用:
考虑如果链接器分配[
char
类型的变量]c
并b
在内存中的同一个单词中(如大多数现代硬件),机器无法加载或存储小于单词的任何内容,可能会发生什么....没有明确定义的合理的内存模型,线程1可能会读取包含单词b
和c
,改c
,写背单词到内存中.同时,线程2也可以这样做b
.然后,无论哪个线程设法首先读取该单词,哪个线程设法将其结果写回内存最后将确定结果....
补充说明
我不相信Stroustrup在讨论缓存行.即使他是,据我所知,缓存一致性协议将透明地处理该问题,除非在硬件I/O期间.
我检查了处理器的硬件数据表.电子方面,我的处理器(英特尔Ivy Bridge)似乎通过某种16位多路复用方案来解决DDR3L内存问题,所以我不知道那是什么意思.我不清楚这与Stroustrup的观点有很大关系.
Stroustrup是一个聪明的人,也是一位杰出的科学家,所以我不怀疑他正在采取一些明智的做法.我很迷惑.
另见这个问题.我的问题在几个方面类似于相关问题,相关问题的答案在这里也很有用.但是,我的问题还在于硬件/总线模型,它促使C++成为它的方式,并导致Stroustrup写出他写的东西.我不是仅仅针对C++标准正式保证的内容寻求答案,而是希望理解为什么C++标准会保证它.什么是潜在的想法?这也是我的问题的一部分.
Pet*_*des 13
我认为这不是一个非常准确,清晰或有用的陈述.更准确地说,现代CPU无法加载或存储小于缓存行的任何内容.(虽然对于不可缓存的内存区域不是这样,例如对于MMIO.)
做一个假设的例子可能会更好,而不是暗示真正的硬件是这样的.但是,如果我们尝试,我们可能会找到一种不那么明显或完全错误的解释,这可能是Stroustrup在写这篇文章以介绍记忆模型主题时的想法.(对不起,这个答案太长了;我最后写了很多东西,同时猜测他的意思和相关主题......)
或者这可能是高级语言设计者不是硬件专家,或者至少偶尔会做出错误陈述的另一种情况.
我认为Stroustrup正在讨论CPU如何在内部工作以实现字节存储指令.他建议没有定义明确且合理的内存模型的CPU 可能会在高速缓存行中使用包含单词的非原子RMW实现字节存储,或者在没有高速缓存的CPU的内存中实现.
即使是关于内部(非外部可见)行为的这种较弱的主张也不适用于我所知道的任何高性能CPU,包括现代x86.现代Intel CPU对字节存储没有吞吐量损失,甚至没有跨越缓存线边界的未对齐字或向量存储.AMD类似.如果其中任何一个必须执行RMW循环,因为存储提交到L1D缓存,它将干扰负载带宽.
Alpha AXP是1992年的高性能RISC设计,在1996 年的 Alpha 21164A(EV56)之前,它是着名的(并且在现代非DSP ISA中独一无二)省略了字节加载/存储指令.显然,他们并不认为word-RMW是实现字节存储的可行选择,因为仅实现32位和64位对齐存储的一个优点是L1D缓存的ECC效率更高. "传统的SECDED ECC在32位颗粒上需要额外7位(开销为22%),而8位颗粒需要额外4位(50%开销)." (@Paul A. Clayton关于字与字节寻址的答案还有一些其他有趣的计算机架构.)如果使用word-RMW实现字节存储,你仍然可以使用字粒度进行错误检测/纠正.
由于这个原因,当前的Intel CPU仅在L1D中使用奇偶校验(不是ECC).请参阅此关于硬件的问答(不)消除"静默存储":在写入之前检查缓存的旧内容,以避免在匹配时将行标记为脏,这将需要RMW而不仅仅是存储,这是一个主要障碍.
我假设其他(非x86)现代CPU设计并未将RMW视为将字节存储提交到L1D缓存的选项.但是,请参阅是否有任何现代/古老的CPU /微控制器,其中缓存的字节存储实际上比字存储慢? 这是完全可能的,一些老/简单/低功耗CPU或微控制器做对字节存储PERF罚入缓存中,即使(我认为以下)是多少,他们做了一个外部RMW到RAM的可能性较小.希望有人会回答这个问题.
Word-RMW也不是MMIO字节存储的有用选项,所以除非你有一个不需要IO子字存储的架构,否则你需要对IO进行某种特殊处理(比如Alpha的稀疏I/O字空间,其中字加载/存储被映射到字节加载/存储,因此它可以使用商用PCI卡而不需要没有字节IO寄存器的特殊硬件).
正如@Margaret指出的那样,DDR3内存控制器可以通过设置屏蔽突发其他字节的控制信号来进行字节存储.将此信息提供给内存控制器(对于未缓存的存储)的相同机制也可以将该信息与加载或存储一起传递到MMIO空间.因此,即使在面向突发的内存系统上也存在真正进行字节存储的硬件机制,并且现代CPU很可能会使用它而不是实现RMW,因为它可能更简单并且对于MMIO正确性要好得多.
执行传输到CPU的长字需要多少和大小的周期,以显示ColdFire微控制器如何用外部信号线发出传输大小(字节/字/长字/ 16字节线)的信号,让它甚至可以进行字节加载/存储如果32位宽的内存连接到其32位数据总线.像这样的东西大概是大多数内存总线设置的典型(但我不知道).ColdFire示例很复杂,还可以配置为使用16位或8位内存,为更广泛的传输采用额外的周期.但是没关系,重要的是它有传输大小的外部信号,告诉内存HW它实际写入哪个字节.
Stroustrup的下一段是
"C++内存模型保证两个执行线程可以更新和访问不同的内存位置而不会相互干扰.这正是我们天真期望的.编译器的工作是保护我们免受有时非常奇怪和微妙的行为的影响.现代硬件.编译器和硬件组合如何实现,这取决于编译器......"
显然他认为真正的现代硬件可能无法提供"安全"的字节加载/存储.设计硬件内存模型的人同意C/C++人员,并意识到如果字节存储指令可以踩到相邻字节,那么它们对程序员/编译器就没有用.
除早期Alpha AXP之外的所有现代(非DSP)架构都具有字节存储和加载指令,而AFAIK这些架构在结构上都定义为不影响相邻字节. 然而,他们在硬件中实现了这一点,软件不需要关心正确性.即使MIPS的第一个版本(1983年)也有字节和半字加载/存储,它是一个非常注重字的ISA.
但是,他实际上并没有声称大多数现代硬件需要任何特殊的编译器支持来实现C++内存模型的这一部分,只是有些人可能会这样做.也许他真的只是讨论第二段中的字可寻址DSP(其中C和C++实现经常使用16或32位char
,正如Stroustrup所讨论的那种编译器工作方式.)
大多数"现代"CPU(包括所有x86)都有一个L1D缓存.它们将获取整个缓存行(通常为64个字节)并在每个缓存行的基础上跟踪脏/非脏. 因此,如果它们都在同一个高速缓存行中,则两个相邻的字节与两个相邻的字几乎完全相同. 写入一个字节或字将导致整行的读取,并最终写回整行.请参阅Ulrich Drepper的每个程序员应该了解的关于记忆的内容.你是正确的MESI(或像MESIF/MOESI这样的衍生物)确保这不是问题.(但同样,这是因为硬件实现了一个理智的内存模型.)
存储只能在线路处于修改状态(MESI)时提交到L1D缓存.因此,即使内部硬件实现对于字节来说很慢并且需要额外的时间将字节合并到高速缓存行中的包含字中,它实际上是原子读取修改写入,只要它不允许该行无效并且重新开始. - 读取和写入之间的获取.(虽然此缓存的行处于"已修改"状态,但其他缓存中没有其他缓存可以具有有效副本).请参阅@ old_timer的评论,并提出相同的观点(但也适用于内存控制器中的RMW).
这比例如原子xchg
或add
来自也需要ALU和寄存器访问的寄存器更容易,因为所涉及的所有HW都处于相同的流水线阶段,其可以简单地停止一个或多个额外的循环.这显然对性能有害,并需要额外的硬件才能让管道阶段发出信号表明它正在停止运行.这并不一定与Stroustrup的第一个主张相冲突,因为他在谈论的是没有记忆模型的假设ISA,但它仍然是一个延伸.
在单核微控制器上,用于缓存字节存储的内部字RMW将更加合理,因为在原子RMW缓存字更新期间不会有来自其他内核的无效请求,它们必须延迟响应.但这对于无法缓存的区域的I/O没有帮助.我说微控制器,因为其他单核CPU设计通常支持某种多插槽SMP.
许多RISC ISA不支持单指令的未对齐字加载/存储,但这是一个单独的问题(当负载跨越两个缓存行甚至页面时,难以处理这种情况,这不会发生在字节或对齐的情况下半字).然而,越来越多的ISA正在为最近版本中的未对齐加载/存储添加有保障的支持.(例如2014年的MIPS32/64第6版,我认为AArch64和最近的32位ARM).
该书的第4版于2013年出版,当时Alpha已经死了多年.第一版于1985年出版,当时RISC是一个新的大创意(例如,1983年斯坦福MIPS,根据维基百科计算硬件的时间表,但当时的"现代"CPU可通过字节存储进行字节寻址.Cyber CDC 6600是字可寻址,可能还在,但不能称为现代.
即使非常面向字的RISC机器(如MIPS和SPARC)也具有字节存储和字节加载(带符号或零扩展)指令.它们不支持未对齐的字加载,简化缓存(或者如果没有缓存则可以访问内存)和加载端口,但是您可以使用一条指令加载任何单个字节,更重要的是存储一个字节而不重写周围的字节.
我认为char
如果针对没有字节存储的Alpha ISA版本,C++ 11(在语言中引入了线程感知内存模型)将需要使用32位.或者它必须使用带有LL/SC的软件atomic-RMW,因为它无法证明没有其他线程可以有一个指针让它们写入相邻的字节.
IDK 在任何CPU中的字节加载/存储指令的速度有多慢,它们在硬件中实现但不像字加载/存储那么便宜.只要您使用movzx/movsx
以避免部分寄存器错误依赖或合并停顿,字节加载在x86上便宜. 在AMD pre-Ryzen上,movsx
/ movzx
需要额外的ALU uop,否则零/符号扩展将在Intel和AMD CPU的加载端口中正确处理.)主要的x86缺点是你需要一个单独的加载指令而不是使用内存操作数作为ALU指令的源(如果你要将一个零扩展字节添加到一个32位整数),节省了前端uop吞吐量带宽和代码大小.或者,如果您只是在字节寄存器中添加一个字节,那么x86基本上没有任何缺点.无论如何,RISC加载存储ISA始终需要单独的加载和存储指令.x86字节存储并不比32位存储更昂贵.
作为性能问题,对于具有慢速字节存储的硬件而言,良好的C++实现可能会将每个字符串放在char
自己的字中,并尽可能使用字加载/存储(例如,对于结构体外的全局变量,以及堆栈中的本地变量).IDK,如果有任何MIPS/ARM的实际实现/无论是慢速字节加载/存储,但如果是这样,gcc可能有-mtune=
控制它的选项.
char[]
char *
当你不知道它可能指向何处时,这没有帮助,或者解除引用.(这包括volatile char*
你用于MMIO的那些.)因此,让编译器+链接器将char
变量放在单独的单词中并不是一个完整的解决方案,只是在真正的字节存储很慢时才会出现性能问题.
PS:关于Alpha的更多信息:
由于很多原因,Alpha很有趣:为数不多的64位ISA之一,而不是现有32位ISA的扩展.作为最新的清洁版ISA之一,Itanium是几年后的另一个尝试了一些巧妙的CPU架构理念.
当引入Alpha架构时,它在RISC架构中是独一无二的,可以避开8位和16位负载和存储.它支持32位和64位加载和存储(长字和四字,在Digital的命名法中).联合建筑师(Dick Sites,Rich Witek)通过引用优势证明了这一决定的合理性:
- 高速缓存和存储器子系统中的字节支持往往会减慢32位和64位数量的访问速度.
- 字节支持使得很难在高速缓存/存储器子系统中构建高速纠错电路.
Alpha通过提供强大的指令来补偿64位寄存器中的字节和字节组.字符串操作的标准基准(例如,一些字节基准测试)表明Alpha在字节操作方面表现非常好.
x86 CPU不仅能够读写单个字节,而且所有现代通用CPU都能够实现这一点.更重要的是,大多数现代CPU(包括x86,ARM,MIPS,PowerPC和SPARC)都能够以原子方式读取和写入单个字节.
我不确定Stroustrup指的是什么.曾经有一些无法进行8位字节寻址的字可寻址机器,如Cray,而且Peter Cordes提到早期的Alpha CPU不支持字节加载和存储,但今天唯一的CPU无法支持字节加载和存储是在特定应用中使用的某些DSP.即使我们假设他意味着大多数现代CPU没有原子字节加载和存储,但对于大多数CPU而言并非如此.
但是,简单的原子加载和存储在多线程编程中没有多大用处.您通常还需要订购保证以及使读取 - 修改 - 写入操作成为原子的方法.另一个考虑因素是,虽然CPU a可能具有字节加载和存储指令,但编译器不需要使用它们.例如,编译器仍然可以生成Stroustrup描述的代码,加载这两个代码b
并c
使用单个字加载指令作为优化.
因此,虽然您确实需要一个定义良好的内存模型,但只有这样才能强制编译器生成您期望的代码,问题不在于现代CPU无法加载或存储任何小于单词的内容.
归档时间: |
|
查看次数: |
1869 次 |
最近记录: |