30 concurrency x86 sse atomic thread-safety
考虑在x86 CPU上进行单个内存访问(单个读取或单个写入,而不是读取或写入)SSE指令.该指令访问16字节(128位)的存储器,访问的存储器位置对齐为16字节.
文档"英特尔®64架构内存订购白皮书"指出,对于"读取或写入地址在8字节边界上对齐的四字(8字节)的指令",内存操作似乎作为单个内存访问执行,而不管记忆类型.
问题:是否存在Intel/AMD/etc x86 CPU,它们保证读取或写入与16字节边界对齐的16字节(128位)作为单个内存访问执行?是这样,它是哪种特定类型的CPU(Core2/Atom/K8/Phenom/...)?如果您对此问题提供答案(是/否),请同时指定用于确定答案的方法 - PDF文档查找,强力测试,数学证明或您用于确定答案的任何其他方法.
此问题涉及http://research.swtch.com/2010/02/off-to-races.html等问题
更新:
我在C中创建了一个可以在您的计算机上运行的简单测试程序.请在您的Phenom,Athlon,Bobcat,Core2,Atom,Sandy Bridge或您碰巧拥有的任何支持SSE2的CPU上编译并运行它.谢谢.
// Compile with:
// gcc -o a a.c -pthread -msse2 -std=c99 -Wall -O2
//
// Make sure you have at least two physical CPU cores or hyper-threading.
#include <pthread.h>
#include <emmintrin.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef int v4si __attribute__ ((vector_size (16)));
volatile v4si x;
unsigned n1[16] __attribute__((aligned(64)));
unsigned n2[16] __attribute__((aligned(64)));
void* thread1(void *arg) {
for (int i=0; i<100*1000*1000; i++) {
int mask = _mm_movemask_ps((__m128)x);
n1[mask]++;
x = (v4si){0,0,0,0};
}
return NULL;
}
void* thread2(void *arg) {
for (int i=0; i<100*1000*1000; i++) {
int mask = _mm_movemask_ps((__m128)x);
n2[mask]++;
x = (v4si){-1,-1,-1,-1};
}
return NULL;
}
int main() {
// Check memory alignment
if ( (((uintptr_t)&x) & 0x0f) != 0 )
abort();
memset(n1, 0, sizeof(n1));
memset(n2, 0, sizeof(n2));
pthread_t t1, t2;
pthread_create(&t1, NULL, thread1, NULL);
pthread_create(&t2, NULL, thread2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
for (unsigned i=0; i<16; i++) {
for (int j=3; j>=0; j--)
printf("%d", (i>>j)&1);
printf(" %10u %10u", n1[i], n2[i]);
if(i>0 && i<0x0f) {
if(n1[i] || n2[i])
printf(" Not a single memory access!");
}
printf("\n");
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我笔记本电脑中的CPU是Core Duo(不是Core2).这个特殊的CPU未通过测试,它实现了16字节的内存读/写,粒度为8字节.输出是:
0000 96905702 10512
0001 0 0
0010 0 0
0011 22 12924 Not a single memory access!
0100 0 0
0101 0 0
0110 0 0
0111 0 0
1000 0 0
1001 0 0
1010 0 0
1011 0 0
1100 3092557 1175 Not a single memory access!
1101 0 0
1110 0 0
1111 1719 99975389
Run Code Online (Sandbox Code Playgroud)
jan*_*neb 33
在英特尔®64和IA-32架构开发人员手册中:Vol.3A,现在包含你提到的内存订购白皮书的规格,如8.2.3.1所述,正如你自己注意到的那样,
The Intel-64 memory ordering model guarantees that, for each of the following memory-access instructions, the constituent memory operation appears to execute as a single memory access: • Instructions that read or write a single byte. • Instructions that read or write a word (2 bytes) whose address is aligned on a 2 byte boundary. • Instructions that read or write a doubleword (4 bytes) whose address is aligned on a 4 byte boundary. • Instructions that read or write a quadword (8 bytes) whose address is aligned on an 8 byte boundary. Any locked instruction (either the XCHG instruction or another read-modify-write instruction with a LOCK prefix) appears to execute as an indivisible and uninterruptible sequence of load(s) followed by store(s) regardless of alignment.
现在,由于上面的列表不包含双四字(16字节)的相同语言,因此架构不保证访问16字节内存的指令是原子的.
话虽如此,最后一段确实提示了一条出路,即带有LOCK前缀的CMPXCHG16B指令.您可以使用CPUID指令确定您的处理器是否支持CMPXCHG16B("CX16"功能位).
在相应的AMD文档中,AMD64技术AMD64架构程序员手册第2卷:系统编程,我找不到类似的清晰语言.
编辑:测试程序结果
(修改测试程序以将#iterations增加10倍)
在Xeon X3450(x86-64)上:
0000 999998139 1572 0001 0 0 0010 0 0 0011 0 0 0100 0 0 0101 0 0 0110 0 0 0111 0 0 1000 0 0 1001 0 0 1010 0 0 1011 0 0 1100 0 0 1101 0 0 1110 0 0 1111 1861 999998428
在Xeon 5150(32位)上:
0000 999243100 283087 0001 0 0 0010 0 0 0011 0 0 0100 0 0 0101 0 0 0110 0 0 0111 0 0 1000 0 0 1001 0 0 1010 0 0 1011 0 0 1100 0 0 1101 0 0 1110 0 0 1111 756900 999716913
在Opteron 2435(x86-64)上:
0000 999995893 1901 0001 0 0 0010 0 0 0011 0 0 0100 0 0 0101 0 0 0110 0 0 0111 0 0 1000 0 0 1001 0 0 1010 0 0 1011 0 0 1100 0 0 1101 0 0 1110 0 0 1111 4107 999998099
这是否意味着英特尔和/或AMD保证这些机器上的16字节内存访问是原子的?恕我直言,它没有.它不是文档中保证的架构行为,因此无法知道在这些特定处理器上16字节内存访问是否真的是原子的,或者测试程序是否因某种原因而无法触发它们.因此依赖它是危险的.
编辑2:如何使测试程序失败
哈!我设法使测试程序失败.在与上面相同的Opteron 2435上,使用相同的二进制文件,但现在通过"numactl"工具运行它,指定每个线程在单独的套接字上运行,我得到:
0000 999998634 5990 0001 0 0 0010 0 0 0011 0 0 0100 0 0 0101 0 0 0110 0 0 0111 0 0 1000 0 0 1001 0 0 1010 0 0 1011 0 0 1100 0 1 Not a single memory access! 1101 0 0 1110 0 0 1111 1366 999994009
那么这意味着什么呢?好吧,Opteron 2435可能会或可能不会保证16字节内存访问对于套接字内访问来说是原子的,但至少在两个套接字之间的HyperTransport互连上运行的缓存一致性协议并不能提供这样的保证.
编辑3:根据"GJ"的要求,线程功能的ASM.
这是为Opteron 2435系统上使用的GCC 4.4 x86-64版本的线程函数生成的asm:
.globl thread2
.type thread2, @function
thread2:
.LFB537:
.cfi_startproc
movdqa .LC3(%rip), %xmm1
xorl %eax, %eax
.p2align 5,,24
.p2align 3
.L11:
movaps x(%rip), %xmm0
incl %eax
movaps %xmm1, x(%rip)
movmskps %xmm0, %edx
movslq %edx, %rdx
incl n2(,%rdx,4)
cmpl $1000000000, %eax
jne .L11
xorl %eax, %eax
ret
.cfi_endproc
.LFE537:
.size thread2, .-thread2
.p2align 5,,31
.globl thread1
.type thread1, @function
thread1:
.LFB536:
.cfi_startproc
pxor %xmm1, %xmm1
xorl %eax, %eax
.p2align 5,,24
.p2align 3
.L15:
movaps x(%rip), %xmm0
incl %eax
movaps %xmm1, x(%rip)
movmskps %xmm0, %edx
movslq %edx, %rdx
incl n1(,%rdx,4)
cmpl $1000000000, %eax
jne .L15
xorl %eax, %eax
ret
.cfi_endproc
Run Code Online (Sandbox Code Playgroud)
为了完整性,.LC3是包含thread2使用的(-1,-1,-1,-1)向量的静态数据:
.LC3:
.long -1
.long -1
.long -1
.long -1
.ident "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
.section .note.GNU-stack,"",@progbits
Run Code Online (Sandbox Code Playgroud)
另请注意,这是AT&T ASM语法,而不是Windows程序员可能更熟悉的英特尔语法.最后,这是march = native,这使得GCC更喜欢MOVAPS; 但没关系,如果我使用march = core2它将使用MOVDQA存储到x,我仍然可以重现失败.
更新:2022 年,Intel 追溯记录了 AVX 功能位意味着对齐的 128 位加载/存储是原子的,至少对于 Intel CPU 而言是这样。AMD 可以记录同样的事情,因为在实践中,他们支持 AVX 的 CPU 已经避免了 8 字节边界的撕裂。请参阅@whatishappened 的答案和 janneb 的更新答案。
具有 AVX 的奔腾和赛扬版本的 CPU 在实践中也将具有相同的原子性,但没有记录的软件检测它的方法。大概还有 Core 2 和 Nehalem,可能还有一些低功耗 Silvermont 系列芯片,这些芯片在 Alder Lake E 核心之前没有 AVX。
因此,最终我们可以__int128
以一种有据可查的方式在 AVX CPU 上进行廉价的原子加载/存储。(因此 C++std::atomic
is_lock_free()
在某些机器上可以返回 true。但不能is_always_lock_free
作为编译时常量,除非 arch 选项生成需要 AVX 的二进制文件。GCC 以前用于lock cmpxchg16b
实现加载/存储,但在 GCC7 IIRC 中进行了更改,不将其宣传为“无锁” “因为它没有您期望的适当支持下的读取端缩放。)
Erik Rigtorp 对最新的 Intel 和 AMD CPU 进行了一些实验测试,以查找撕裂情况。结果位于https://rigtorp.se/isatomic/。请记住,没有关于此行为的文档或保证(超出 128 位或在非 AVX CPU 上),并且 IDK 是否可能使用此类 CPU 的自定义多插槽计算机的原子性低于他测试的计算机。但在当前的 x86 CPU(不是 K10)上,对齐加载/存储的 SIMD 原子性只是随高速缓存和 L1d 高速缓存之间的数据路径宽度进行缩放。
x86 ISA 仅保证高达 8B 的原子性,因此实现可以自由地实现 SSE / AVX 支持,就像 Pentium III / Pentium M / Core Duo 所做的那样:内部数据以 64 位一半处理。一个 128 位存储由两个 64 位存储完成。在 Yonah 微架构(Core Duo)中,进出缓存的数据路径只有 64b 宽。(来源:Agner Fog 的微架构文档)。
最近的实现确实具有更宽的内部数据路径,并将 128b 指令作为单个操作处理。Core 2 Duo (conroe/merom) 是第一个具有 128b 数据路径的 Intel P6 后裔微架构。(我不知道 P4,但幸运的是它已经足够老了,完全不相关。)
这就是为什么 OP 发现 128b 运算在 Intel Core Duo (Yonah) 上不是原子的,但其他发帖者发现它们在后来的 Intel 设计中是原子的,从 Core 2 (Merom) 开始。
Realworldtech 这篇关于 Merom 与 Yonah 的文章中的图表显示了 Merom(和 P4)中 ALU 和 L1 数据缓存之间的 128 位路径,而低功耗 Yonah 具有 64 位数据路径。在所有 3 种设计中,L1 和 L2 高速缓存之间的数据路径均为 256b。
数据路径宽度的下一个跳跃来自英特尔的Haswell,具有 256b (32B) AVX/AVX2 加载/存储,以及 L1 和 L2 缓存之间的 64 字节路径。我预计 256b 加载/存储在 Haswell、Broadwell 和 Skylake 中是原子的,但我没有要测试的。
Skylake-AVX512 具有 512 位数据路径,因此它们至少在读/写 L1d 缓存方面也自然是原子的。环形总线(客户端芯片)以 32 字节块传输,但 Intel 保证 32B 半块之间不会出现撕裂,因为只要不跨越缓存行边界,它们就能保证任何未对齐情况下 8 字节加载/存储的原子性。
Zen 4 将 512 位操作分为两半,因此可能不是 512 位原子性。
正如 janneb 在他出色的实验答案中指出的那样,多核系统中套接字之间的缓存一致性协议可能比共享末级缓存 CPU 中的协议更窄。对于广泛的加载/存储,对原子性没有架构要求,因此设计人员可以自由地将它们在套接字内设为原子性,但如果方便的话,可以在套接字之间设为非原子性。我不知道 AMD 的 Bulldozer 系列或英特尔的插槽间逻辑数据路径有多宽。(我说“逻辑”,因为即使数据以较小的块传输,它也可能不会修改缓存行,直到它被完全接收。)
查找有关 AMD CPU 的类似文章应该可以得出关于 128b 操作是否是原子操作的合理结论。只需检查指令表就会有一些帮助:
K8 解码movaps reg, [mem]
为 2 m-ops,而 K10 和推土机系列将其解码为 1 m-op。AMD 的低功耗 bobcat 将其解码为 2 个 ops,而 jaguar 将 128b movaps 解码为 1 m-op。(它支持类似于推土机系列 CPU 的 AVX1:256b insns(甚至 ALU 操作)被拆分为两个 128b 操作。Intel SnB 仅拆分 256b 加载/存储,同时具有全宽 ALU。)
janneb 的 Opteron 2435 是一款6 核 Istanbul CPU,属于 K10 系列的一部分,因此这个单 m-op -> 原子结论在单个插槽内显得准确。
Intel Silvermont 使用单个 uop 执行 128b 加载/存储,每个时钟的吞吐量为 1。这与整数加载/存储相同,因此它很可能是原子的。
《AMD架构程序员手册第一卷:应用程序编程》第3.9.1节中说:“CMPXCHG16B
可用于在64位模式下执行16字节原子访问(有一定的对齐限制)”。
不过,上证所指令却没有这样的评论。事实上,4.8.3 中有一条注释说 LOCK 前缀“与 128 位媒体指令一起使用时会导致无效操作码异常”。因此,我认为 AMD 处理器不保证 SSE 指令的原子 128 位访问,并且进行原子 128 位访问的唯一方法是使用CMPXCHG16B
.
《Intel 64 和 IA-32 架构软件开发人员\xe2\x80\x99s 手册第 3A 卷:系统编程指南,第 1 部分》在 8.1.1 中说“访问大于四字的数据的 x87 指令或 SSE 指令可能是使用多个内存访问来实现。” 可以肯定的是,128 位 SSE 指令不能由 ISA 保证原子性。 英特尔文档第 2A 卷说道CMPXCHG16B
:“该指令可以与 LOCK 前缀一起使用,以允许原子地执行该指令。”
此外,CPU 制造商尚未针对特定 CPU 型号发布原子 128b SSE 操作的书面保证。
\n 归档时间: |
|
查看次数: |
7100 次 |
最近记录: |