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