说到C的并发内存模型,Stroustrup的 *C编程语言,* 第4版,第41.2.1、表示:
.(像大多数现代硬件)这台机器不能加载或存储任何小于一个字。
但是,我的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在说什么?
引用的内容更长
下面是斯特劳斯特鲁普在更完整的背景下的报价:
考虑一下,如果一个链接器在内存中的同一个字中分配了[char
类型的变量,如] c
和b
,并且(像大多数现代硬件一样)机器不能加载或存储任何小于一个字的内容,会发生什么......如果没有定义良好且合理的内存模型,线程1可能会读取包含b
和c
的字,更改c
,然后将该字写回到内存中。与此同时,线程2也可以对b
执行相同的操作。然后,哪个线程先读取了这个字,哪个线程最后将结果写回内存,就决定了结果......
其他备注
我不认为Stroustrup是在谈论缓存行。据我所知,即使他是这样,缓存一致性协议也会透明地处理这个问题,除非是在硬件I/O期间。
我已经查看了处理器的硬件数据表。从电学上讲,我的处理器(英特尔Ivy Bridge)似乎通过某种16位复用方案来寻址DDR3L内存,所以我不知道这是怎么回事。不过,我不清楚这与斯特劳斯特鲁普的观点有多大关系。
斯特劳斯特鲁普是一个聪明人,一个杰出的科学家,所以我不怀疑他是在做一些明智的事情。我糊涂了。
另请参阅this question.我的问题在几个方面与链接问题相似,链接问题的答案在这里也很有用。然而,我的问题还涉及到硬件/总线模型,它促使C成为现在的样子,并导致Stroustrup写他所写的东西。我并不只是在寻求C标准形式上所保证的问题的答案,我还希望理解为什么C++标准会保证它。潜在的想法是如何的?这也是我问题的一部分。
6条答案
按热度按时间sauutmhj1#
**TL:DR:在每一个有字节存储指令的现代伊萨(包括x86)上,它们都是原子的,不会干扰周围的字节。
实际的实现机制(in non-x86 CPUs)有时是一个内部RMW周期来修改缓存行中的整个字,但这是在核心内部“无形”完成的,而它对该高速缓存行具有独占所有权,因此它只是一个性能问题,而不是正确性问题。(存储缓冲区中的合并有时可以将字节存储指令转换为高效的L1 d缓存全字提交。
关于Stroustrup的措辞
我不认为这是一个非常准确,清晰或有用的声明。更准确地说,现代CPU不能加载或存储任何小于缓存行的东西。(虽然这对于不可缓存的内存区域不是真的,例如对于MMIO。)
最好是举一个假设的例子来讨论内存模型,而不是暗示真实的的硬件是这样的。但如果我们尝试,我们也许可以找到一个解释,不是那么明显或完全错误的,这可能是Stroustrup在写这篇文章介绍记忆模型时所想的。(抱歉这个回答这么长;我最后写了很多,同时猜测他可能是什么意思,以及关于相关的主题...)
或者,这可能是高级语言设计人员不是硬件Maven的另一个例子,或者至少偶尔会做出错误的陈述。
我认为Stroustrup是在谈论CPU如何在内部实现字节存储指令。他建议没有定义良好和合理的内存模型的CPU * 可能会在缓存行中实现包含字的非原子RMW的字节存储,或者在没有缓存的CPU的内存中实现字节存储。
**即使是这种关于内部(外部不可见)行为的较弱声明,对于高性能x86 CPU也是不正确的。现代英特尔CPU不会对字节存储造成吞吐量损失,甚至不会对不跨越缓存行边界的未对齐字或向量存储造成吞吐量损失。AMD也是如此。
如果字节或未对齐的存储必须在存储提交到L1 D缓存时进行RMW循环,则它将以我们可以用性能计数器测量的方式干扰存储和/或加载指令/uop吞吐量。(在一个精心设计的实验中,避免了在提交到L1 d缓存之前在存储缓冲区中合并存储的可能性,从而隐藏了成本,因为存储执行单元在当前CPU上每个时钟只能运行1次存储。
但是,**一些非x86 ISA的高性能设计确实使用原子RMW周期来在内部将存储提交到L1 d缓存。Are there any modern CPUs where a cached byte store is actually slower than a word store?**高速该高速缓存行始终处于MESI独占/已修改状态,因此它不会引入任何正确性问题,只会对性能造成很小的影响。这与从其他CPU执行存储操作非常不同。(下面关于 that 不发生的论点仍然适用,但我的更新可能错过了一些仍然认为原子缓存RMW不太可能的东西。
(On在许多非x86 ISA中,完全不支持未对齐的存储,或者与x86软件相比很少使用。弱序ISA允许在存储缓冲区中进行更多合并,因此实际上不会有那么多字节存储指令导致单字节提交到L1 d。如果没有这些花哨(耗电)的高速缓存访问硬件的动机,分散字节存储的字RMW在某些设计中是可以接受的折衷。
Alpha AXP是1992年的高性能RISC设计,著名的(在现代非DSP ISA中是独一无二的)省略了字节加载/存储指令,直到1996年的Alpha 21164 A(EV 56)。显然,他们不认为字RMW是实现字节存储的可行选择,因为仅实现32位和64位对齐存储的一个优点是L1 D缓存的ECC更有效。(@Paul A.克莱顿关于word vs.字节寻址还有一些其他有趣的计算机体系结构。)如果字节存储是用字RMW实现的,你仍然可以用字粒度进行错误检测/纠正。
因此,当前的英特尔CPU仅在L1 D中使用奇偶校验(而不是ECC)。(至少一些较老的Xeons可以在ECC模式下以一半的容量运行L1 d,而不是正常的32 KiB,如discussed on RWT。目前尚不清楚是否有任何变化,例如。英特尔现在使用ECC进行L1 d)。另请参阅this Q&A关于硬件(不)消除“静默存储”:在写入之前检查缓存的旧内容以避免在匹配时将行标记为脏,这将需要RMW而不仅仅是存储,这是一个主要障碍。
事实证明,一些高性能的流水线设计确实使用原子字RMW来提交到L1 d,尽管它会拖延内存流水线,但是(正如我在下面所讨论的那样)任何外部可见的RMW都不太可能提交到RAM。
Word-RMW对于MMIO字节存储也不是一个有用的选项,所以除非你的架构不需要子字存储用于IO,否则你需要某种特殊的IO处理(比如Alpha's sparse I/O space,字加载/存储被Map到字节加载/存储,所以它可以使用普通PCI卡,而不是需要特殊的硬件,没有字节IO寄存器)。
正如@Margaret所指出的,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的下一段是
“C内存模型保证了两个执行线程可以更新和访问不同的内存位置,而不会相互干扰。这正是我们天真地期望的。编译器的工作是保护我们免受现代硬件有时非常奇怪和微妙的行为。编译器和硬件组合如何实现这一点取决于编译器。...”
因此,显然他认为真实的的现代硬件可能无法提供“安全”的字节加载/存储。设计硬件内存模型的人同意C/C的人,并意识到字节存储指令对程序员/编译器来说,如果它们可以踩在相邻的字节上,它们就不会很有用了。
**除了早期的Alpha AXP之外,所有现代(非DSP)架构都有字节存储和加载指令,AFAIK这些都在架构上定义为不影响相邻字节。**无论它们在硬件上实现了这一点,软件都不需要关心正确性。即使是MIPS的第一个版本(1983年)也有字节和半字加载/存储,它是一个非常面向字的伊萨。
然而,他实际上并没有声称大多数现代硬件需要任何特殊的编译器支持来实现C内存模型的这一部分,只是说 * 一些 * 可能会。也许他真的只是在第二段中谈论字寻址DSP(其中C和C实现经常使用16或32位
char
,这正是Stroustrup所谈论的那种编译器解决方案)。大多数“现代”CPU(包括所有x86)都有L1 D缓存。它们将提取整个缓存行(通常为64字节)并在每个缓存行的基础上跟踪脏/非脏。**所以两个相邻字节与两个相邻字几乎完全相同,如果它们都在同一个缓存行中。**写入一个字节或字将导致整行的提取,并最终将整行写回。参见Ulrich Drepper的What Every Programmer Should Know About Memory。MESI(或MESIF/MOESI等衍生工具)确保这不是问题,这是正确的。(但同样,这是因为硬件实现了一个健全的内存模型。
存储仅可在行处于(MESI的)已修改状态时提交到L1 D高速缓存。因此,即使内部硬件实现对于字节来说是缓慢的,并且花费额外的时间来将字节合并到该高速缓存行中的包含字中,只要它不允许行在读取和写入之间被无效和重新获取,它实际上就是一个 * 原子 * 读取修改写入。(While this cache has the line in Modified state, no other cache can have a valid copy)。请参阅@old_timer的评论,也表达了同样的观点(但也适用于内存控制器中的RMW)。
这比例如更容易。来自寄存器的原子
xchg
或add
,该寄存器还需要ALU和寄存器访问,因为涉及的所有硬件都在同一流水线级中,这可能会简单地拖延一两个额外的周期。这显然不利于性能,并且需要额外的硬件来允许流水线阶段发出它正在停止的信号。这并不一定与Stroustrup的第一个主张相冲突,因为他谈论的是一个没有内存模型的假设伊萨,但这仍然是一个延伸。在单核微控制器上,用于缓存字节存储的内部字RMW将更合理,因为不会有来自其他核心的无效请求,它们必须在原子RMW缓存字更新期间延迟响应。但这对不可缓存区域的I/O没有帮助。我之所以说微控制器,是因为其他单核CPU设计通常支持某种多插槽SMP。
许多RISC ISA不支持使用单个指令的未对齐字加载/存储,但这是一个单独的问题(困难在于处理加载跨越两个缓存行甚至页面的情况,这不能发生在字节或对齐的半字上)。不过,越来越多的ISA在最近的版本中添加了对非对齐加载/存储的保证支持。(例如2014年的MIPS 32/64 Release 6,我认为AArch 64和最近的32位ARM。也是RISC-V,但它允许用一个陷阱来实现未对准处理程序)。
Stroustrup的书的The 4th edition出版于2013年,当时Alpha已经去世多年。第一版是published in 1985,当时RISC是一个新的大概念(例如,published in 1985)。1983年的斯坦福大学MIPS,according to Wikipedia's timeline of computing HW,但当时的“现代”CPU是通过字节存储进行字节寻址的。Cyber CDC 6600是字寻址的,可能仍然存在,但不能被称为现代。
甚至像MIPS和SPARC这样非常面向字的RISC机器也有字节存储和字节加载(带符号或零扩展)指令。它们不支持未对齐的字加载,简化该高速缓存(或内存访问,如果没有缓存)和加载端口,但您可以用一条指令加载任何单个字节,更重要的是 * 存储 * 一个字节,而无需对周围字节进行任何架构可见的非原子重写。(缓存存储可以在存储缓冲区中合并,以潜在地完成4字节或8字节块的完整写入,从而避免在这种情况下需要对该提交进行特殊处理。
我假设C++11(它在语言中引入了线程感知的内存模型)在Alpha上需要使用32位
char
,如果目标是没有字节存储的Alpha伊萨。或者,当它不能证明没有其他线程可以有一个指针,让他们写相邻的字节时,它将不得不使用LL/SC的软件原子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++实现的硬件与缓慢的字节存储可能会把每个
char
在自己的字,并使用字加载/存储只要可能(例如)。对于结构外的全局变量,以及对于堆栈上的局部变量)。IDK如果MIPS / ARM /无论什么的任何真实的实现都有缓慢的字节加载/存储,但如果是这样,也许gcc有-mtune=
选项来控制它。这对
char[]
没有帮助,或者当您不知道char *
可能指向哪里时取消引用它。(这包括您将用于MMIO的volatile char*
。)因此,让编译器+链接器将char
变量放在单独的字中并不是一个完整的解决方案,如果真正的字节存储很慢,这只是一个性能黑客。PS:关于Alpha的更多信息:
Alpha之所以有趣,有很多原因:这是为数不多的全新64位ISA之一,而不是对现有32位ISA的扩展。最近的一个全新的ISA,Itanium是几年后的另一个,它尝试了一些整洁的CPU架构思想。
从the Linux Alpha HOWTO。
当Alpha架构被引入时,它在RISC架构中是独一无二的,因为它避免了8位和16位的加载和存储。它支持32位和64位的加载和存储(在Digital的命名法中为长字和四字)。联合架构师(Dick Sites,Rich Witek)通过引用优势来证明这一决定:
1.该高速缓存和存储器子系统中的字节支持倾向于减慢对32位和64位数量的访问。
1.字节支持使得难以将高速纠错电路构建到该高速缓存/存储器子系统中。
Alpha通过提供强大的指令来进行补偿,以便在64位寄存器内操作字节和字节组。字符串操作的标准基准测试(例如,一些Byte基准测试)表明Alpha在字节操作上表现得非常好。
6uxekuva2#
不仅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无法加载或存储任何小于单词的东西。
g6baxovj3#
作者似乎担心线程1和线程2会陷入读-修改-写的情况(不是在软件中,软件执行两个字节大小的单独指令,在线路的某处逻辑必须执行读取-修改-写入)而不是理想的读取修改写入读取修改写入,变成读、读、修改、修改、写、写或一些其它定时,使得读修改前的版本和最后一个写入的版本都获胜。读读修改修改写写或读修改读修改写写或读修改读写修改写。
问题是从0x 1122开始,一个线程希望将其设置为0x 33 XX,另一个线程希望将其设置为0xXX 44,但是对于例如读取读取修改写入写入,您最终将获得0x 1144或0x 3322,而不是0x 3344
一个健全的(系统/逻辑)设计就不会有这个问题,当然不是像这样的通用处理器,我曾在设计与时序问题,但这不是我们在这里谈论的,完全不同的系统设计不同的目的。读-修改-写在正常的设计中不会跨越足够长的距离,而x86是正常的设计。
读-修改-写将发生在非常接近所涉及的第一个SRAM的位置(当以典型的方式运行x86时,理想情况下是L1,操作系统能够运行C编译的多线程程序),并且在几个时钟周期内发生,因为RAM处于理想的总线速度。正如Peter所指出的,这被认为是整个高速缓存行在该高速缓存内经历的,而不是处理器内核和高速缓存之间的读取-修改-写入。
即使在多核系统中,“同时”的概念也不一定是同时的,最终你会得到序列化,因为性能不是基于它们从头到尾都是并行的,而是基于保持总线加载。
引号是说变量分配给内存中的同一个字,所以这是同一个程序。两个独立的程序不会共享这样的地址空间。所以
欢迎你来尝试一下,做一个多线程程序,一个写地址0xnnn 00000,另一个写地址0xnnnn 00001,每个写,然后读,或者更好的几个写相同的值比一个读,检查读的是他们写的字节,然后用不同的值重复。让它运行一段时间,小时/天/周/月。看看你是否在系统中出错......对实际的写指令使用汇编,以确保它正在做你要求的事情(而不是C或任何编译器,它做或声称它不会把这些项目放在同一个词中)。可以增加延迟以允许更多的缓存回收,但这会降低“同时”发生冲突的几率。
你的例子,只要你确保你不是坐在一个边界(缓存,或其他)的两侧,如0xNNNNFFFFF和0xNNNN 00000,隔离两个字节写地址,如0xNNNN 00000和0xNNNN 0001有指令背靠背,看看你是否得到一个读读修改写写。围绕它进行一个测试,这两个值在每个循环中都是不同的,你可以在任何你想要的延迟后将单词作为一个整体读回,并检查这两个值。重复几天/几周/几个月/几年,看看是否失败。阅读您的处理器执行和微代码功能,了解它如何处理此指令序列,并根据需要创建不同的指令序列,尝试在处理器内核远端的少数几个时钟周期内启动事务。
编辑
引号的问题在于,这都是关于语言和的使用。“像大多数现代硬件一样”把整个主题/文本置于一个敏感的位置,它太模糊了,一方可以争辩说我所要做的就是找到一个真实的案例来使所有其余的都是真实的,同样,一方可以争辩说如果我找到一个案例,其余的都不是真实的。使用像这样的词有点混乱,作为一个可能的出狱卡。
现实情况是,我们的数据中有很大一部分存储在DRAM中的8位宽存储器中,只是我们不能以8位宽访问它们,通常我们一次访问8个,64位宽。在几周/几个月/几年/几十年后,这种说法将是不正确的。
大的引号说“同时”,然后说读……首先写...最后一个,第一个,最后一个,同时,没有意义,它是并行的还是串行的?上下文作为一个整体关注上面的读、读、修改、修改、写、写的变化,其中最后有一次写入,并且取决于何时该一次读取确定两次修改是否发生。与此同时,“像大多数现代硬件一样”没有意义,如果它们瞄准存储器中的同一触发器/晶体管,那么在单独的核心/模块中实际上并行开始的事情最终会被串行化,一个最终必须等待另一个先去。作为物理学的基础,我不认为这是不正确的,在未来几周/几个月/几年。
f0brbegy4#
这是正确的。“x86_64 CPU,就像原始的x86 CPU一样,无法从rsp读取或写入任何小于一个(在本例中为64位)字的内容。并且它通常不会读或写少于一整条高速缓存线,尽管有一些方法可以绕过该高速缓存,尤其是在写的时候(见下文)。
不过,在本文中,Stroustrup指的是潜在的数据竞争(在可观察的层面上缺乏原子性)。由于您提到该高速缓存一致性协议,此正确性问题在x86_64上不相关。换句话说,是的,CPU仅限于全字传输,* 但是 * 这是透明处理的,作为程序员,您通常不必担心。事实上, C++ (从C++11开始)* 保证 * 不同内存位置上的并发操作具有明确定义的行为,即你所期待的那种。即使硬件不能保证这一点,实现也必须通过生成可能更复杂的代码来找到一种方法。
也就是说,出于以下两个原因,最好将整个字甚至缓存行始终涉及到机器级别的事实放在脑后。
volatile
关键字对于防止此类不适当的优化至关重要。下面是一个非常糟糕的数据结构的例子,有点做作。假设您有16个线程解析一个文件中的一些文本。每个线程都有一个0到15的
id
。字符串
这是安全的,因为每个线程在不同的内存位置上运行。然而,这些存储器位置通常将驻留在同一高速缓存线上,或至多被分割成两个高速缓存线。然后,使用该高速缓存一致性协议来正确地同步对
c[id]
的访问。问题就在这里,因为这会迫使每个 * 其他 * 线程在对c[id]
执行任何操作之前,等待该高速缓存行变为独占可用,除非它已经在“拥有”缓存行的内核上运行。假设有几个,例如16的核,则高速缓存一致性通常将始终该高速缓存行从一个核传送到另一个核。由于显而易见的原因,这种效应被称为“高速缓存线乒乓”。它会造成可怕的性能瓶颈。这是一个非常糟糕的 * 虚假共享 * 案例的结果,即线程共享物理高速缓存行而不实际访问相同的逻辑存储器位置。与此相反,特别是如果采取额外的步骤来确保
file
数组驻留在其自己的缓存行上,则从性能Angular 来看,使用它将是完全无害的(在x86_64上),因为大多数情况下,指针只是从其读取的。在这种情况下,多个内核可以该高速缓存行“共享”为只读。只有当任何内核尝试写入该高速缓存行时,它才必须告诉其他内核它将“占用”缓存行以进行独占访问。(This大大简化了,因为存在不同级别的CPU高速缓存,并且多个内核可能共享同一个L2或L3高速缓存,但它应该能给予您对问题有一个基本的了解。)
vqlkdk9b5#
不知道斯特劳斯特鲁普说的“单词”是什么意思。可能是机器的最小内存大小?
无论如何,并不是所有的机器都是用8位(字节)分辨率创建的。事实上,我推荐Eric S的这篇很棒的文章。Raymond描述了计算机的一些历史:http://www.catb.org/esr/faqs/things-every-hacker-once-knew/
“......人们过去也普遍认为,36位架构解释了C语言的一些不幸的特性。最初的Unix机器PDP-7的特点是18位字对应于更大的36位计算机上的半字。这些更自然地表示为六个八进制(3位)数字。
ryoqjall6#
Stroustrup并不是说没有机器可以执行比其原生字长更小的加载和存储,他是说机器不能。
虽然这一点一开始看起来令人惊讶,但这并不是什么深奥的东西。
对于初学者,我们将忽略该高速缓存层次结构,稍后将考虑到这一点。
假设CPU和内存之间没有缓存。
内存的大问题是 * 密度 *,试图把更多的位可能到最小的面积。
为了实现这一点,从电气设计的Angular 来看,尽可能宽地暴露总线是很方便的(这有利于重用一些电信号,但我还没有查看具体细节)。
因此,在需要大存储器(如x86)或简单的低成本设计是有利的架构中(例如,涉及RISC机器的架构),存储器总线大于最小的可寻址单元(通常为字节)。
根据项目的预算和传统,存储器可以单独使用更宽的总线,或者与一些边带信号沿着使用,以选择特定的单元。
这实际上意味着什么?
如果您查看datasheet of a DDR3 DIMM,您会发现有64 * DQ 0-DQ 63 * 引脚用于读取/写入数据。
这是数据总线,64位宽,一次8字节。
这8字节在x86架构中有着非常好的基础,英特尔在其优化手册的WC部分中提到了这一点,其中指出数据是从64 * 字节 * 填充缓冲区传输的(请记住:我们现在忽略高速缓存,但这类似于高速缓存线如何以8字节的突发(希望是连续的)被写回)。
这是否意味着x86只能写入QWORDS(64位)?
否,相同的数据表显示,每个DIMM都有 * DM 0-DM 7、DQ 0-DQ 7 * 和 * DQS 0-DQS 7 * 信号来屏蔽、引导和选通64位数据总线中的每个8字节。
因此,x86可以以本机方式和原子方式读写字节。
然而,现在很容易看出,这不可能是每个体系结构的情况。
例如,VGA视频存储器是DWORD(32位)可寻址的,使其适合8086的字节可寻址世界导致了混乱的位平面。
在一般的专用架构中,如DSP,在硬件级不可能有字节可寻址存储器。
还有一个转折:我们刚刚讨论了内存数据总线,这是可能的最低层。
一些CPU可以具有在字可寻址存储器之上构建字节可寻址存储器的指令。
这是什么意思?
加载单词的一小部分很容易:就丢弃其余的字节!
不幸的是,我想不起这个建筑的名字了(如果它真的存在的话!其中处理器通过阅读包含未对齐字节的对齐字并在将其保存在寄存器中之前旋转结果来模拟未对齐字节的加载。
对于商店来说,情况更为复杂:如果我们不能简单地写刚刚更新的那部分单词,那么我们还需要写未更改的剩余部分。
CPU或编程器必须读取旧的内容,对其进行更新并将其写回。
这是一个读-修改-写操作,是讨论原子性时的核心概念。
请考虑:
字符串
是否存在数据竞赛?
这在x86上是安全的,因为它们可以写入字节,但如果体系结构不能呢?
两个线程都必须读取 * 整个 *
foo
数组,修改它并将其写回。在 * 伪C * 中,这将是
型
我们现在可以明白斯特劳斯特鲁普所说的话了:两个存储器
*((int*)foo) = tmpX
彼此阻碍,为了看到这一点,考虑以下可能的执行序列:型
如果C没有一个 * 内存模型 *,那么这些麻烦就会是实现特定的细节,从而使C在多线程环境中成为一种无用的编程语言。
考虑到玩具例子中描述的情况有多普遍,Stroustrup * 强调了 * 一个定义明确的记忆模型的重要性。
形式化一个内存模型是一项艰苦的工作,这是一个令人疲惫、容易出错和抽象的过程,所以我也从Stroustrup的话中看到了一点自豪。
我还没有温习C++内存模型,但更新了不同的数组元素is fine。
这是一个非常有力的保证。
我们省略了缓存,但这并没有真正改变什么,至少对于x86情况是这样。
x86通过高速缓存写入内存,高速缓存以64 * 字节 * 为一行收回。
在内部,每个内核可以原子地更新任何位置处的行,除非加载/存储跨越行边界(例如,通过在其结尾附近书写)。
这可以通过自然地调整数据来避免(你能证明吗?).
在多代码/套接字环境中,该高速缓存一致性协议确保一次仅允许一个CPU自由地写入缓存的内存行(使其处于独占或已修改状态的CPU)。
基本上,MESI协议族使用类似于锁定DBMS的概念。
为了写入,这具有将不同的存储器区域“分配”给不同的CPU的效果。
所以这并不影响上面的讨论。