gpt4 book ai didi

c++ - 现代x86硬件能否不将单个字节存储到内存中?

转载 作者:行者123 更新时间:2023-12-02 03:51:19 25 4
gpt4 key购买 nike

谈到C++的并发内存模型,Stroustrup的C++编程语言,第4版,Sect。 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
[...]

评论是我的,但 Assembly 是GCC的。当然,它运行良好。

显然,当Stroustrup解释说硬件可以加载并存储小于一个字的内容时,我不明白他在说什么。据我所知,我的程序除了加载和存储小于一个字的对象外什么也不做。

C++对零成本,硬件友好抽象的深入关注使C++与其他易于掌握的编程语言脱颖而出。因此,如果Stroustrup在公交车上有一个有趣的信号心理模型,或者有其他类似的东西,那么我想了解Stroustrup的模型。

请问Stroustrup在说什么?

带上下文的更大报价

这是Stroustrup在完整上下文中的报价:

Consider what might happen if a linker allocated [variables of char type like] c and b 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 containing b and c, change c, and write the word back into memory. At the same time, thread 2 could do the same with b. 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在谈论缓存行。就我所知,即使他是高速缓存一致性协议(protocol),也可以透明地处理该问题,除非在硬件I/O期间。

我已经检查了处理器的硬件数据表。从电气上讲,我的处理器(一个英特尔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可能在缓存行或没有缓存的CPU的内存中使用包含字的非原子RMW来实现字节存储。

对于高性能x86 CPU ,即使对内部(外部不可见)行为的这种虚弱说法也不是正确的。现代的Intel CPU不会对字节存储,甚至是未对齐的字或 vector 存储(不跨越高速缓存行边界)的吞吐量造成损失。 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上增加存储空间的操作有很大的不同。 (以下关于未发生这种情况的论点仍然适用,但是我的更新可能遗漏了一些仍然认为原子高速缓存-RMW不太可能的东西。)

(在许多非x86 ISA上,根本不支持未对齐的存储,或者使用未对齐的存储比在x86软件中使用的存储少。弱排序的ISA允许在存储缓冲区中进行更多的合并,因此实际上,由于字节存储指令的数量较少,因此单行存储没有这些花哨的(耗电的)高速缓存访​​问硬件动机,在某些设计中,用于分散字节存储的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,尽管它使内存流水线停滞了,但是(正如我在下文所述),任何对RAM进行外部可见的RMW的可能性都较小。

Word-RMW也不是MMIO字节存储的有用选项,因此,除非您的体系结构不需要IO的子字存储,否则您需要对IO进行某种特殊处理(例如Alpha's sparse I/O space,其中要加载字)/stores被映射到字节加载/stores,因此它可以使用商用PCI卡,而不需要没有字节IO寄存器的特殊硬件。

@Margaret points out一样,DDR3存储器 Controller 可以通过设置掩盖突发的其他字节的控制信号来进行字节存储。将这些信息获取到内存 Controller (用于未缓存的存储)的相同机制也可以使该信息与装入或存储一起传递到MMIO空间。因此,有一些硬件机制可以真正做到
一个字节存储,即使是在面向突发的存储系统上,现代CPU也很可能会使用它而不是实现RMW,因为它可能更简单,并且对MMIO的正确性要好得多。

How many and what size cycles will be needed to perform longword transferred to the CPU展示了ColdFire微 Controller 如何通过外部信号线发送传输大小(字节/字/长字/16字节线)的信号,允许它进行字节加载/存储,即使32位宽的内存已连接到其32位位数据总线。对于大多数内存总线设置来说,大概是这样的事情(但我不知道)。 ColdFire示例也很复杂,因为它还可以配置为使用16位或8位内存,从而花费更多的时间进行更广泛的传输。但请记住,重要的一点是它具有用于传输大小的外部信号,以告知存储器HW实际正在写入哪个字节。

Stroustrup的next paragraph

"The C++ memory model guarantees that two threads of execution can update and access separate memory locations without interfering with each other. 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位char,就像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提出相同的观点(但对于内存 Controller 中的RMW也是如此)。

这比例如容易来自寄存器的原子xchgadd,它也需要ALU和寄存器访问权限,因为涉及的所有硬件都在同一个流水线阶段,这可能会停顿一个或两个额外的周期。这显然对性能不利,并且需要额外的硬件以允许该管道阶段表明其正在停止。这不一定与Stroustrup的第一个主张相抵触,因为他在谈论的是没有内存模型的假设ISA,但这仍然很麻烦。

在单核微 Controller 上,用于高速缓存字节存储的内部字RMW更加合理,因为在原子RMW高速缓存字更新期间,不会有必须延迟响应的其他内核发出的Invalidate请求。但这对无法缓存区域的I/O没有帮助。我之所以说微 Controller ,是因为其他单核CPU设计通常支持某种多路SMP。

许多RISC ISA不支持通过一条指令进行未对齐字的加载/存储,但这是一个单独的问题(难点是当加载跨越两个缓存行甚至页面时,字节或对齐都不会发生这种情况半字)。不过,越来越多的ISA在最近版本中增加了对未对齐的加载/存储的保证支持。 (例如2014年的MIPS32/64 Release 6,我认为是AArch64和最近的32位ARM)。

Stroustrup的书The 4th edition于2013年出版,当时阿尔法已经去世多年。第一版是published in 1985,当时RISC是一个新的大想法(例如1983年的Stanford MIPS,according to Wikipedia's timeline of computing HW,但是当时的“现代” CPU可以用字节存储进行字节寻址。Cyber​​CDC 6600可以用字寻址,并且可能仍然存在,但不能称为现代。

甚至像MIPSSPARC这样的面向单词的RISC机器也具有字节存储和字节加载(带符号或零扩展名)指令。它们不支持不对齐字加载,简化了缓存(如果没有缓存,则简化了内存访问)和加载端口,但是您可以使用一条指令加载任何单个字节,更重要的是,可以存储一个字节,而无需任何架构可见的非字节原子重写周围的字节。 (尽管缓存的商店可以

我想如果针对不带字节存储的Alpha ISA版本,则Alpha上的C++ 11(向语言引入了线程感知的内存模型)将需要使用32位char。否则,如果无法证明没有其他线程可以通过指针写入相邻字节,则它必须与LL/SC一起使用atomic-RMW软件。

IDK在以硬件实现它们的任何CPU中字节加载/存储指令的速度如何,但不如wordt/rstrong字加载/存储便宜。只要您使用movzx/movsx避免部分注册错误的依赖关系或合并停顿,在x86上字节加载就很便宜。 x86的主要缺点是,您需要单独的加载指令,而不是将内存操作数用作ALU指令的源(如果您要向32位整数添加零扩展字节),则可以节省前端uop吞吐量带宽和代码大小。或者,如果您只是将一个字节添加到字节寄存器,则x86基本上没有任何缺点。 RISC负载存储ISA始终始终需要单独的负载和存储指令。 x86字节存储不再比32位存储昂贵。

作为性能问题,对于具有慢字节存储的硬件而言,良好的C++实现可能会将每个movsx放入其自己的字中,并在可能的情况下使用字加载/存储(例如,针对结构外部的全局变量以及堆栈中的局部变量)。 IDK(如果有MIPS/ARM/任何真正的实现)或慢速字节加载/存储,但如果是,则gcc可以使用movzx选项来控制它。

On AMD pre-Ryzen, char / -mtune= needs an extra ALU uop, but otherwise zero/sign extension is handled right in the load port on Intel and AMD CPUs.,或者在您不知道char[]指向何处时取消引用。 (这包括您要用于MMIO的char *。)因此,让编译器和链接器将volatile char*变量放在单独的单词中并不是一个完整的解决方案,如果真正的字节存储速度很慢,则仅仅是性能上的麻烦。

PS:有关Alpha的更多信息:

Alpha之所以有趣,有很多原因:它是为数不多的64位ISA之一,而不是对现有32位ISA的扩展。也是较新的ISA之一,Itanium是几年后的另一种,它尝试了一些简洁的CPU体系结构构想。

From the Linux Alpha HOWTO.

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:

  1. Byte support in the cache and memory sub-system tends to slow down accesses for 32-bit and 64-bit quantities.
  2. 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.

关于c++ - 现代x86硬件能否不将单个字节存储到内存中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46721075/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com