请考虑以下代码,该代码使用std::atomic
原子地加载64位对象。
#include <atomic>
struct A {
int32_t x, y;
};
A f(std::atomic<A>& a) {
return a.load(std::memory_order_relaxed);
}
使用GCC,发生了很多事情,并生成了以下代码。 (https://godbolt.org/z/zS53ZF)
f(std::atomic<A>&):
mov rax, QWORD PTR [rdi]
ret
这正是我所期望的,因为在这种情况下,我认为没有理由不应该将64位结构像其他任何64位字一样对待。
然而,对于Clang,故事却不同。 Clang生成以下内容。 (https://godbolt.org/z/d6uqrP)
f(std::atomic<A>&): # @f(std::atomic<A>&)
push rax
mov rsi, rdi
mov rdx, rsp
mov edi, 8
xor ecx, ecx
call __atomic_load
mov rax, qword ptr [rsp]
pop rcx
ret
mov rdi, rax
call __clang_call_terminate
__clang_call_terminate: # @__clang_call_terminate
push rax
call __cxa_begin_catch
call std::terminate()
这对我来说是有问题的,原因如下:
__atomic_load
的调用,这意味着我的二进制文件需要与libatomic链接。这意味着我需要不同的库列表来链接,具体取决于我的代码用户使用的是GCC还是Clang。我现在想到的一个重要问题是,是否有办法让Clang还将负载转换为一条指令。我们将其用作计划分发给他人的库的一部分,因此我们不能依赖于所使用的特定编译器。到目前为止,向我建议的解决方案是使用punning类型并将结构与64位int一起存储在并集内,因为Clang确实在一条指令中以原子方式正确加载了64位int。但是,我对此解决方案表示怀疑,因为尽管它似乎可以在所有主要的编译器上使用,但我已经读到它实际上是未定义的行为。如果其他人不熟悉该技巧,那么这样的代码也不是特别适合其他人阅读和理解。
总而言之,有没有一种方法可以原子方式加载64位结构:
此Clang错过的优化仅在libstdc ++中发生;就像我们对-stdlib=libc++
的期望一样,在Godbolt内联上发出叮当声。 https://godbolt.org/z/Tt8XTX。
似乎对结构进行64位对齐足以握住clang。
libstdc++
的std::atomic
模板针对自然对齐时足够小的原子类型进行此操作,但是clang ++可能只看到基础类型的对齐方式,而不是atomic<T>
的类成员。 libstdc ++实现。我还没有调查有人应将此报告给clang / LLVM bugzilla。
#include <atomic>
#include <stdint.h> // you forgot this header.
struct A {
alignas(int64_t) int32_t x;
int32_t y; // this one must be separate, otherwise y would also be aligned -> 16-byte object
};
A f(std::atomic<A>& a) {
return a.load(std::memory_order_relaxed);
}
# clang++ 9.0 -std=gnu++17 -O3; g++ is the same
f(std::atomic<A>&):
mov rax, qword ptr [rdi]
ret
顺便说一句,不,libatomic
库函数将不使用锁;它确实知道8字节对齐的负载自然是原子的,其他使用线程将使用普通负载/存储,而不是锁。
较旧的clang至少使用call __atomic_load_8
而不是通用的可变大小的clang,但这仍然是一个很大的优化遗漏。