考虑一个像这样的数组atomic<int32_t> shared_array[]
。如果你想要 SIMD 矢量化怎么办for(...) sum += shared_array[i].load(memory_order_relaxed)
?。或者在数组中搜索第一个非零元素,或者将其范围归零?这可能很少见,但请考虑任何不允许元素内撕裂但元素之间重新排序的用例。(也许是寻找 CAS 候选人)。
I think x86 aligned vector loads/stores would be safe in practice to use on for SIMD with mo_relaxed
operations, because any tearing will only happen at 8B boundaries at worst on current hardware (because that's what makes naturally-aligned 8B accesses atomic1). Unfortunately Intel's manuals only say:
“访问大于四字的数据的 x87 指令或 SSE 指令可以使用多个内存访问来实现。”
无法保证这些组件访问自然对齐、不重叠或其他任何情况。 (有趣的事实:x87 10 字节fld m80
在 Haswell 上使用 2 个负载微指令和 2 个 ALU 微指令完成负载,根据阿格纳·福格的说法,大概是 qword + word。)
如果您想以一种面向未来的方式进行矢量化,当前的 x86 手册称该方式将适用于所有未来的 x86 CPU,您可以使用 8B 块加载/存储movq
/ movhps
.
或者也许你可以使用 256bvpmaskmovd带着真实的面具,因为手册的操作部分将其定义为多个单独的 32 位负载,例如Load_32(mem + 4)
。这是否意味着每个元素都充当单独的 32 位访问,保证该元素内的原子性?
(在真实的硬件上,Haswell 上有 1 个负载和 2 个 port5 uops,或者在 Ryzen 上只有 1 或 2 个负载 + ALU uops (128 / 256)。我认为这是针对不需要从以下元素中抑制异常的情况:进入未映射的页面,因为这可能会更慢(但我不知道它是否需要微码辅助)。无论如何,这告诉我们它至少与正常的原子一样vmovdqa
在 Haswell 上加载,但这并没有告诉我们关于 x86 Deathstation 9000 的任何信息,其中 16B / 32B 向量访问被分解为单字节访问,因此每个元素内可能存在撕裂。
我认为实际上可以安全地假设您不会在 16、32 或 64 位元素中看到撕裂aligned矢量加载/存储在任何真正的 x86 CPU 上,因为这对于已经必须保持自然对齐的 64 位标量存储原子性的高效实现没有意义,但是了解手册中的保证实际上有多远很有趣.)
聚集 (AVX2,AVX512) / 分散 (AVX512)
指令如vpgatherdd
更明显的是由多个单独的 32b 或 64b 访问组成。 AVX2 形式已记录就像做多个FETCH_32BITS(DATA_ADDR);
因此,据推测,这已被通常的原子性保证所覆盖,并且如果每个元素不跨越边界,则将自动收集每个元素。
AVX512聚集记录在英特尔的 PDF insn 参考手册中 as
DEST[i+31:i] <- MEM[BASE_ADDR + SignExtend(VINDEX[i+31:i]) * SCALE + DISP]), 1)
分别为每个元素。 (排序:元素可以按任何顺序收集,但故障必须按从右到左的顺序传递。其他指令的内存排序遵循 Intel-
64 内存排序模型。)
AVX512 scatters以相同的方式记录(上一个链接的第 1802 页)。没有提到原子性,但它们确实涵盖了一些有趣的极端情况:
如果两个或多个目的地索引完全重叠,则可以跳过“较早”的写入。
元素可以按任何顺序分散,但故障必须按从右到左的顺序传递
如果该指令覆盖自身然后发生故障,则之前只能完成元素的子集
故障已传递(如上所述)。如果故障处理程序完成并尝试重新执行此操作
指令,将执行新指令,并且分散不会完成。
仅保证对重叠向量索引的写入相对于彼此有序(从 LSB 到
源寄存器的 MSB)。请注意,这还包括部分重叠的向量索引。写的不是
重叠可以按任何顺序发生。其他指令的内存排序遵循 Intel-64 内存
订购型号。请注意,这并没有考虑映射到同一物理层的非重叠索引。
地址位置。
(即,因为相同的物理页被映射到两个不同虚拟地址的虚拟内存中。因此允许在地址转换之前(或与之并行)进行重叠检测,而无需在地址转换之后重新检查。)
我将最后两个纳入其中,是因为它们是有趣的极端案例,我什至没有想过要考虑。自我修改的案例很搞笑,但我认为rep stosd
会有同样的问题(它也是可中断的,使用rcx
跟踪进度)。
我认为原子性是 Intel-64 内存排序模型的一部分,因此他们提到它并且没有说任何其他内容的事实似乎暗示每个元素的访问是原子的。 (收集两个相邻的 4B 元素几乎肯定不算作一次 8B 访问。)
x86 手册保证哪些向量加载/存储指令在每个元素的基础上是原子的?
在真实硬件上进行的实验测试几乎肯定会告诉我,我的 Skylake CPU 上的一切都是原子的,但这不是这个问题的目的。我问我对手册的解释是否正确vmaskmov
/ vpmaskmov
负载,以及收集/分散。
(如果有任何理由怀疑真正的硬件将继续是简单的元素原子原子movdqa
负载,这也将是一个有用的答案。)
- 脚注:x86 原子性基础知识:
在 x86 中,8B 或更窄的自然对齐加载和存储保证是原子的,根据Intel和AMD手册。事实上,对于缓存访问,任何不跨越 8B 边界的访问也是原子的。 (在 Intel P6 及更高版本上,提供了比 AMD 更强的保证:缓存行(例如 64B)内未对齐对于缓存访问来说是原子的)。
16B 或更宽的向量加载/存储不保证是原子的。它们位于某些 CPU 上(至少对于当观察者是其他 CPU 时的缓存访问而言),但即使是对 L1D 缓存的 16B 宽原子访问也不会使其成为原子的。例如,AMD K10 Opterons 插槽之间的 HyperTransport 一致性协议在对齐的 16B 向量的两半之间引入撕裂,即使对同一套接字(物理 CPU)中的线程进行测试显示没有撕裂。
(如果您需要完整的 16B 原子加载或存储,您可以使用以下命令进行破解lock cmpxchg16b
就像海湾合作委员会所做的那样std::atomic<T>
,但那是terrible为了性能。也可以看看x86_64 上的原子双浮点或 SSE/AVX 矢量加载/存储.)