当前位置: 首页 > 知识库问答 >
问题:

x86上的原子性

史淳
2023-03-14

英特尔64和IA-32处理器提供锁定信号,在某些关键内存操作期间自动断言该信号,以锁定系统总线或等效链路。当该输出信号被断言时,来自其他处理器或总线代理的总线控制请求被阻止。软件可以指定在锁语义后面加上指令的锁前缀的其他情况。

它来自《英特尔手册》第3卷

听起来,内存上的原子操作将直接在内存(RAM)上执行。我很困惑,因为我在分析汇编输出时看到“没有什么特别的”。基本上,为std::atomic生成的程序集输出

因此,我提出了我的疑问,但主要问题是:


共有2个答案

宋志学
2023-03-14

LOCK#信号(cpu包/套接字的引脚)用于旧芯片(用于LOCK前缀原子操作),现在有缓存锁。对于更复杂的原子操作,如. Exchange。fetch_add您将使用LOCK前缀或其他类型的原子指令(cmpxchg/8/16?)进行操作。

同样的手册,系统编程指南部分:

在奔腾4、英特尔至强和P6系列处理器中,锁定操作由缓存锁或总线锁处理。如果内存访问是可缓存的,并且仅影响单个缓存线,则会调用缓存锁,并且在操作过程中不会锁定系统总线和系统内存中的实际内存位置

您可以查阅Paul E.McKenney的论文和书籍:*现代微处理器中的内存排序,2007*内存障碍:软件黑客的硬件视图,2010*perfbook,“并行编程难吗?如果是,您能做些什么?”

和*英特尔64架构内存订购白皮书,2007年。

x86/x86_64需要内存屏障,以防止加载重新排序。第一篇论文:

x86(...AMD64与x86兼容...(由于x86 CPU提供进程排序,因此所有CPU都同意给定CPU写入内存的顺序,因此smp_wmb()原语对于CPU[7]来说是一个无操作。但是,需要编译器指令来防止编译器执行会导致跨smp_wmb()基元重新排序的优化。

另一方面,x86 CPU传统上没有为负载提供排序保证,因此smp_mb()smp_rmb()基元扩展为lock; addl。这个原子指令作为负载和存储的屏障。

什么是读取内存障碍(来自第二篇论文):

这样做的效果是,读取内存屏障命令仅在执行它的CPU上加载,因此,读取内存屏障之前的所有加载似乎都在读取内存屏障之后的任何加载之前完成。

例如,“英特尔64体系结构内存订购白皮书”

“英特尔64内存排序”保证,对于以下每一条内存访问指令,组成内存操作都会作为单个内存访问执行,而不管内存类型如何:。。。读取或写入地址在4字节边界上对齐的双字(4字节)的指令。

Intel 64内存排序遵循以下原则:1.负载不会与其他负载重新排序。...5.在多处理器系统中,内存排序服从因果关系(内存排序尊重传递可见性)。...英特尔64内存排序确保按程序顺序查看负载

此外,mfence的定义:http://www.felixcloutier.com/x86/MFENCE.html

对在MFENCE指令之前发出的所有“从内存加载”和“存储到内存”指令执行序列化操作。此序列化操作可确保在MFENCE指令之后的任何加载或存储指令之前,以程序顺序在MFENCE指令之前的每个加载和存储指令都全局可见。

轩辕佑运
2023-03-14

听起来,内存上的原子操作将直接在内存(RAM)上执行。

不,只要系统中的每个可能的观察者都将操作视为原子操作,该操作就只能涉及缓存。

对于原子读修改写操作(如lock add[mem],eax,尤其是对于未对齐的地址),满足这一要求要困难得多,此时CPU可能会断言锁信号。在asm中您仍然看不到更多:硬件实现了locked指令所需的ISA语义。

虽然我怀疑在现代CPU上是否有一个物理的外部LOCK#引脚,其中内存控制器内置于CPU,而不是单独的北桥芯片。

std::原子

编译器不支持顺序cst加载。

我想我曾读到,旧的MSVC在某一点上确实为此发出了MFENCE(也许是为了防止重新订购未受隔离的NT商店?或者代替商店?)。但事实并非如此:我测试了MSVC19.00.23026.0。在该程序的asm输出中查找foo和bar,该程序在联机编译中转储自己的asm

这里我们不需要Geofence的原因是x86内存模型不允许LoadStore和LoadLoad重新排序。早期(非seq_cst)存储仍然可以延迟到seq_cst加载之后,所以它不同于使用独立的std::atomic_thread_fence(mo_seq_cst);之前的X. load(mo_acquire);

如果我理解正确的话,X.store(2)只是mov[某处],2

这与您的想法是一致的,即加载需要mitch;seq_cst加载或存储中的一个或另一个需要一个完整的屏障来防止不允许StoreLoad重新排序,否则可能会发生这种情况。

在实践中,编译器开发者选择便宜的负载(mov)/昂贵的存储(mov m的),因为负载更常见。C 11到处理器的映射。

(x86内存排序模型是程序顺序加上带有存储转发的存储缓冲区(另请参阅)。这使得asm中的mo_acquiremo_release是免费的,只需要阻止编译时重新排序,并让我们选择是将MFENCE完全隔离放置在加载还是存储上。)

因此,seq_cst存储要么是movmire,要么是xchg。为什么具有顺序一致性的std::atomic存储使用XCHG?讨论xchg在某些CPU上的性能优势。在AMD上,MFENCE(IIRC)被记录为有额外的串行化管道语义学(用于指令执行,而不仅仅是内存排序)来阻止乱序执行,在一些实践中的英特尔处理器(Skylake)上也是如此。

MSVC对存储的ASM与clang相同,使用xchg使用相同的指令执行存储内存屏障。

原子释放或轻松存储可以是mov,它们之间的区别只是允许多少编译时重新排序。

这个问题类似于早期C语言中的内存模型的第2部分:顺序一致性和原子性,您在其中提出了以下问题:

正如您在问题中指出的,原子性与任何其他操作的排序无关。(即内存\u顺序\u松弛)。它只是意味着操作作为一个不可分割的操作发生,因此得名,而不是作为多个部分发生,可以部分发生在其他操作之前,部分发生在其他操作之后。

您可以“免费”获得原子性,而无需额外的硬件来进行对齐加载或存储,最多可存储核心、内存和I/O总线(如PCIe)之间的数据路径大小。i、 e.不同级别的缓存之间,以及不同内核的缓存之间。在现代设计中,内存控制器是CPU的一部分,因此即使是访问内存的PCIe设备也必须通过CPU的系统代理。(这甚至可以让Skylake的eDRAM L4(在任何桌面CPU中都不可用:()用作内存侧缓存(与Broadwell不同,Broadwell将其用作L3 IIRC的牺牲品缓存),位于内存和系统中其他所有内容之间,因此它甚至可以缓存DMA)。

这意味着CPU硬件可以做任何必要的事情,以确保存储或加载相对于系统中可以观察到它的任何其他东西是原子的。这可能不多,如果有的话。DDR内存使用一条足够宽的数据总线,64位对齐的存储器确实在同一周期内通过内存总线电连接到DRAM。由于PCIe和CPU之间的通信长度不一样,所以它不能像CPU和CPU之间的通信一样,直接在CPU和CPU之间传输,这一点并不重要。但无论如何,这是“免费”部分:不需要临时阻止其他请求来保持原子传输的原子性。

x86保证最多64位的对齐加载和存储是原子的,但不是更广泛的访问。低功耗实现可以自由地将向量加载/存储拆分为64位块,就像从PIII到奔腾M的P6一样。

记住,原子只是意味着所有观察者都认为它已经发生或没有发生,从来没有部分发生过。不要求它立即到达主内存(或者,如果很快被覆盖的话)。原子地修改或读取一级缓存足以确保任何其他内核或DMA访问将看到对齐的存储或加载作为单个原子操作发生。如果此修改发生在存储执行之后很长时间(例如,由于无序执行而延迟,直到存储退役),则可以。

像Core2这样到处都有128位路径的现代CPU通常具有原子SSE 128b加载/存储,这超出了x86 ISA的保证。但请注意,多插槽Opteron上有一个有趣的例外,可能是由于超传输。这证明了原子化修改一级缓存不足以为比最窄数据路径(在本例中不是一级缓存和执行单元之间的路径)更宽的存储提供原子性。

对齐很重要:跨越缓存线边界的加载或存储必须在两个单独的访问中完成。这使它成为非原子的。

x86保证缓存的访问最多8字节是原子的,只要它们不跨越AMD/Intel上的8B边界。(或者对于仅在P6及更高版本上的英特尔,不要跨越缓存线边界)。这意味着整个缓存线(现代CPU上的64B)在英特尔上原子地传输,尽管它比Haswell/Skylake上的L2和L3之间的数据路径更宽(32B)。这种原子性在硬件中并不是完全“自由”的,可能需要一些额外的逻辑来防止负载读取仅部分传输的缓存行。虽然缓存行传输只发生在旧版本无效之后,所以当传输发生时,核心不应该从旧副本中读取。AMD可以在实践中撕开更小的边界,也许是因为使用了不同的MESI扩展,可以在缓存之间传输脏数据。

对于更广泛的操作数,比如原子式地将新数据写入结构的多个条目中,您需要使用锁来保护它,所有对它的访问都要遵守该锁。(您可以使用x86lock cmpxchg16b和重试循环来执行原子16b存储。请注意,如果没有互斥锁,就无法模拟它。)

原子读修改写是它变得更难的地方

相关:我关于num可以是'intnum'的原子吗?更详细地介绍了这一点。

每个核心都有一个私有的L1缓存,该缓存与所有其他核心一致(使用MOESI协议)。高速缓存线在高速缓存和主存储器的级别之间以大小从64位到256位的块进行传输(这些传输实际上可能是整个高速缓存线粒度上的原子传输?)

要执行原子RMW,核心可以将L1缓存的一行保持在修改状态,而不接受对加载和存储之间受影响的缓存行的任何外部修改,系统的其余部分将视操作为原子。(因此它是原子的,因为通常的乱序执行规则要求本地线程看到自己的代码按程序顺序运行。)

它可以通过在原子RMW运行时不处理任何缓存一致性消息(或者一些更复杂的版本,允许其他操作有更多的并行性)来做到这一点。

未对齐的locked操作是一个问题:我们需要其他内核来查看对两个缓存线的修改作为单个原子操作发生。这可能需要实际存储到DRAM,并使用总线锁。(AMD的优化手册说,当缓存锁不够时,CPU上会发生这种情况。)

 类似资料:
  • 问题内容: 今天,每个现代OS都提供一些原子操作: Windows具有API FreeBSD有 Solaris有 Mac OS X具有 像Linux一样的东西吗? 我需要它在大多数Linux支持的平台上工作,包括:x86,x86_64和 arm 。 我至少在GCC和Intel编译器上需要它。 我不需要使用像glib或qt这样的3rd par库。 我需要它才能在C ++中工作(不需要C) 问题: G

  • 我读过,x86的INC指令不是原子指令。我的问题是为什么会这样?假设我们在x86-64上递增一个64位整数,我们可以用一条指令来递增,因为INC指令同时处理内存变量和寄存器。那么为什么它不是原子的呢?

  • 在多核x86机器上,假设在core1上执行的线程增加一个整数变量,同时Core2上的线程也增加它。假设的初始值为0,那么它最终是否总是?或者它还有其他价值?假设被声明为,并且我们没有使用原子变量(例如原子变量) 如果在这种情况下的值实际上总是2,这是否意味着x86-64中的也将具有相同的属性,即最终总是2?

  • 问题内容: 有人告诉我: 在x86-64下,FP算法是通过SSE完成的,因此long double是64位。 但是在x86-64 ABI中它表示: 参见:amd64-abi.pdf 和gcc说是16并给出= 和 所以我很困惑,64位怎么样?我认为这是一个80位的表示形式。 问题答案: 在x86-64下,FP算法是通过SSE完成的,因此long double是64位。 这就是 通常发生 X86-64

  • 问题内容: 在Linux的通用x86用户态应用程序上,什么会导致SIGBUS(总线错误)?我在网上可以找到的所有讨论都与内存对齐错误有关,据我了解,这实际上并不适用于x86。 (我的代码在Geode上运行,以防那里存在任何相关的特定于处理器的怪癖。) 问题答案: 如果打开未对齐的访问陷阱,则可以从未对齐的访问中获取SIGBUS,但是通常在x86上是关闭的。如果出现某种错误,也可以通过访问内存映射的

  • 基于 Swoole 提供的 Swoole\Atomic,直接在配置文件中设置,就可以在worker进程中使用,数据互通。 使用时无需加锁! 配置方式 在项目配置文件中加入以下节 'atomics' => [ // 定义名为name1的,初始值为0 'name1', // 定义名为name2的,初始值为10 'name2' => 10, ],