SSE指令:哪些CPU可以进行原子16B内存操作?

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,我仍然可以重现失败.

  • @GJ.:我不知道你想说的是什么,但无论如何,如果处理器在内部实现一个16字节的存储指令作为存储管道中的2个8字节存储(因为它允许按照架构保证进行)在编程手册中提供,另一个处理器完全可以"窃取"两个商店之间的缓存线.不太可能,但并非不可能,从我在答案中显示的失败测试中可以看出. (3认同)
  • @PeterCordes:我刚刚发现在Intel手册第3A卷(从答案中链接)的当前版本(2021年11月,虽然它可能已经早些时候引入,但我没有遵循它)中,某些16字节内存操作如果 CPU 支持 AVX,则保证是原子的。 (2认同)

Pet*_*des 6

更新: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。这与整数加载/存储相同,因此它很可能是原子的。


Ant*_*ams 5

《AMD架构程序员手册第一卷:应用程序编程》第3.9.1节中说:“CMPXCHG16B可用于在64位模式下执行16字节原子访问(有一定的对齐限制)”。

\n\n

不过,上证所指令却没有这样的评论。事实上,4.8.3 中有一条注释说 LOCK 前缀“与 128 位媒体指令一起使用时会导致无效操作码异常”。因此,我认为 AMD 处理器不保证 SSE 指令的原子 128 位访问,并且进行原子 128 位访问的唯一方法是使用CMPXCHG16B.

\n\n

Intel 64 和 IA-32 架构软件开发人员\xe2\x80\x99s 手册第 3A 卷:系统编程指南,第 1 部分》在 8.1.1 中说“访问大于四字的数据的 x87 指令或 SSE 指令可能是使用多个内存访问来实现。” 可以肯定的是,128 位 SSE 指令不能由 ISA 保证原子性。 英特尔文档第 2A 卷说道CMPXCHG16B:“该指令可以与 LOCK 前缀一起使用,以允许原子地执行该指令。”

\n\n

此外,CPU 制造商尚未针对特定 CPU 型号发布原子 128b SSE 操作的书面保证。

\n

  • 您可以使用“CMPXCHG16B”来读取值。首先将任何值加载到“RDX:RAX”中,然后将**相同**值加载到“RCX:RBX”中。所有寄存器都为零即可。然后执行“CMPXCHG16B [地址]”。如果值匹配,则存储回相同的值。如果它们不匹配,则“RDX:RAX”将更新为实际值。无论哪种方式,“RDX:RAX”都会保留原始存储的值,并且内存不会改变。 (5认同)