16 字节(在某些第一代 AMD K8 上不可用,因此对于 gcc,您必须使用 -mcx16 或 -march=whatever 来启用它)。
gcc 为exchange() 提供了最好的汇编:
double exchange(double x) {
return ad.exchange(x); // seq_cst
}
movq rax, xmm0
xchg rax, QWORD PTR ad[rip]
movq xmm0, rax
ret
// in 32-bit code, compiles to a cmpxchg8b retry loop
void atomic_add1() {
// ad += 1.0; // not supported
// ad.fetch_or(-0.0); // not supported
// have to implement the CAS loop ourselves:
double desired, expected = ad.load(std::memory_order_relaxed);
do {
desired = expected + 1.0;
} while( !ad.compare_exchange_weak(expected, desired) ); // seq_cst
}
mov rax, QWORD PTR ad[rip]
movsd xmm1, QWORD PTR .LC0[rip]
mov QWORD PTR [rsp-8], rax # useless store
movq xmm0, rax
mov rax, QWORD PTR [rsp-8] # and reload
.L8:
addsd xmm0, xmm1
movq rdx, xmm0
lock cmpxchg QWORD PTR ad[rip], rdx
je .L5
mov QWORD PTR [rsp-8], rax
movsd xmm0, QWORD PTR [rsp-8]
jmp .L8
.L5:
ret
compare_exchange 总是进行按位比较,因此您不必担心负零 (-0.0) 与 IEEE 语义中的 +0.0 比较相等,或者 NaN 是无序的。但是,如果您尝试检查 desired == expected 并跳过 CAS 操作,这可能是一个问题。对于足够新的编译器,memcmp(&expected, &desired, sizeof(double)) == 0 可能是在 C++ 中表达 FP 值的按位比较的好方法。只要确保避免误报;假阴性只会导致不必要的 CAS。
硬件仲裁lock or [mem], 1 绝对比在lock cmpxchg 重试循环上旋转多个线程要好。每次内核访问高速缓存行但失败时,其cmpxchg 的吞吐量与整数内存目标操作相比,一旦获得高速缓存行就总是成功。
IEEE 浮点数的一些特殊情况可以通过整数运算来实现。例如atomic<double> 的绝对值可以用 lock and [mem], rax 完成(其中 RAX 设置了符号位以外的所有位)。或者通过将 1 与符号位进行或运算来强制浮点数/双精度数为负。或者用 XOR 切换它的符号。您甚至可以使用lock add [mem], 1 原子地将其幅度增加 1 ulp。 (但前提是您可以确定它不是从无穷大开始...nextafter() 是一个有趣的功能,这要归功于 IEEE754 的非常酷的设计,它带有偏差指数,使得从尾数到指数的进位实际上可以工作。)
可能没有办法在 C++ 中表达这一点,让编译器在使用 IEEE FP 的目标上为您完成。因此,如果您想要它,您可能必须自己使用类型双关语到 atomic<uint64_t> 或其他东西,并检查 FP 字节序是否与整数字节序等匹配(或者仅针对 x86 执行此操作。大多数其他目标都有LL/SC 而不是内存目标锁定操作。)
尚不能支持原子 AVX/SSE 向量之类的东西,因为它依赖于 CPU
正确。无法通过缓存一致性系统检测 128b 或 256b 存储或加载何时是原子的。 (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70490)。即使是在 L1D 和执行单元之间进行原子传输的系统,在通过窄协议在缓存之间传输缓存线时也会在 8B 块之间撕裂。真实示例:a multi-socket Opteron K10 with HyperTransport interconnects 似乎在单个套接字中具有原子 16B 加载/存储,但不同套接字上的线程可以观察到撕裂。
但是,如果您有一个对齐的doubles 共享数组,您应该能够在它们上使用矢量加载/存储,而不会在任何给定的double 内“撕裂”。
Per-element atomicity of vector load/store and gather/scatter?
我认为可以安全地假设对齐的 32B 加载/存储是通过不重叠的 8B 或更宽的加载/存储完成的,尽管英特尔不保证这一点。对于未对齐的操作,假设任何事情可能都不安全。
如果您需要 16B 原子负载,您唯一的选择是 lock cmpxchg16b,desired=expected。如果成功,它将用自己替换现有值。如果失败,那么您将获得旧内容。 (极端情况:只读内存上的这个“加载”错误,所以要小心传递给执行此操作的函数的指针。)此外,与可能离开的实际只读加载相比,性能当然是可怕的处于共享状态的缓存行,并且不是完整的内存屏障。
16B atomic store 和 RMW 都可以使用lock cmpxchg16b 显而易见的方式。这使得纯存储比常规向量存储贵得多,特别是如果 cmpxchg16b 必须重试多次,但原子 RMW 已经很昂贵。
与lock cmpxchg16b相比,将矢量数据移入/移出整数regs的额外指令不是免费的,但也不昂贵。
# xmm0 -> rdx:rax, using SSE4
movq rax, xmm0
pextrq rdx, xmm0, 1
# rdx:rax -> xmm0, again using SSE4
movq xmm0, rax
pinsrq xmm0, rdx, 1
在 C++11 中:
atomic<__m128d> 即使对于只读或只写操作(使用cmpxchg16b)也会很慢,即使实现最佳。 atomic<__m256d> 甚至不能无锁。
alignas(64) atomic<double> shared_buffer[1024]; 理论上仍然允许对读取或写入它的代码进行自动矢量化,只需要 movq rax, xmm0 然后 xchg 或 cmpxchg 用于 double 上的原子 RMW。 (在 32 位模式下,cmpxchg8b 可以工作。)不过,您几乎肯定不会从编译器那里得到好的 asm!
您可以原子地更新一个 16B 的对象,但原子地分别读取 8B 的一半。 (我认为这对于 x86 上的内存排序是安全的:请参阅我在 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80835 的推理)。
但是,编译器没有提供任何简洁的方式来表达这一点。我破解了一个适用于 gcc/clang 的联合类型双关语:How can I implement ABA counter with c++11 CAS?。但是 gcc7 和更高版本不会内联 cmpxchg16b,因为他们正在重新考虑 16B 对象是否真的应该将自己呈现为“无锁”。 (https://gcc.gnu.org/ml/gcc-patches/2017-01/msg02344.html)。