Clang 不会内联 std::atomic::load 来加载 64 位结构
Clang doesn't inline std::atomic::load for loading 64-bit structs
考虑以下代码,它使用 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 linked。这意味着我需要不同的库列表 link 取决于我的代码的用户是使用 GCC 还是 Clang。
库函数可能使用了锁,这会降低性能
我现在想到的一个重要问题是是否有办法让 Clang 也将负载转换为一条指令。我们将其用作我们计划分发给其他人的库的一部分,因此我们不能依赖所使用的特定编译器。到目前为止向我建议的解决方案是使用类型双关并将结构与 64 位 int 一起存储在联合中,因为 Clang 确实在一条指令中以原子方式正确加载 64 位 int。然而,我对这个解决方案持怀疑态度,因为尽管它似乎适用于所有主要编译器,但我已经读到它实际上是未定义的行为。如果其他人不熟悉这个技巧,这样的代码对于其他人阅读和理解也不是特别友好。
总而言之,有没有一种方法可以自动加载 64 位结构:
- 适用于 Clang 和 GCC,最好适用于大多数其他流行的编译器,
- 编译时生成一条指令,
- 不是未定义的行为,
- reader友善吗?
这个 clang 错过的优化只发生在 libstdc++ 上;正如我们对 -stdlib=libc++
所期望的那样,在 Godbolt 内联上发出 clang。 https://godbolt.org/z/Tt8XTX.
看来给struct 64位对齐就可以hand-hold clang了
libstdc++
的 std::atomic
模板针对自然对齐时小到足以成为原子的类型执行此操作,但也许 clang++ 只看到基础类型的对齐,而不是 class atomic<T>
的成员,在 libstdc++ 实现中。我没有调查过;有人应该将此报告给 clang / LLVM bugzilla。
#include <atomic>
#include <stdint.h> // you forgot this header.
struct A {
alignas(std::atomic_int64_t) int32_t x; // same alignment as std::atomic uses for atomic<int64_t>
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);
}
与 std::atomic<int64_t>
相同的对齐应该在每个目标上提供足够的对齐,其中 64 位对象完全可以是无锁的。 alignof(int64_t)
在 32 位 ABI 上可能只有 4,我没有使用 alignas(8)
来避免在 char 为 32 位且 sizeof(int64_t) = 的系统上过度对齐2.
alignas(2*sizeof(int32_t))
总是自然地对齐结构,即使在 atomic_int64_t 没有对齐的目标上,例如因为它不是无锁的。没关系。
Godbolt; clang13.0
仍然需要此解决方法
# clang++ 9.0 -std=gnu++17 -O3; g++ is the same
f(std::atomic<A>&):
mov rax, qword ptr [rdi]
ret
顺便说一句,不,libatomic
库函数不会使用锁;它确实知道 8 字节对齐的加载自然是原子的,并且其他使用线程将使用普通 loads/stores,而不是锁。
较旧的 clang 至少使用 call __atomic_load_8
而不是通用的可变大小的,但这仍然是一个很大的优化失误。
有趣的事实:clang -m32
将使用 lock cmpxchg8b
来实现 8 字节原子加载,而不是像 GCC 那样使用 SSE 或 fild
。但这与使用 SSE 或 x87 是 ABI 兼容的,因此它不会被锁定到使用次优方式。 :/
考虑以下代码,它使用 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 linked。这意味着我需要不同的库列表 link 取决于我的代码的用户是使用 GCC 还是 Clang。 库函数可能使用了锁,这会降低性能
我现在想到的一个重要问题是是否有办法让 Clang 也将负载转换为一条指令。我们将其用作我们计划分发给其他人的库的一部分,因此我们不能依赖所使用的特定编译器。到目前为止向我建议的解决方案是使用类型双关并将结构与 64 位 int 一起存储在联合中,因为 Clang 确实在一条指令中以原子方式正确加载 64 位 int。然而,我对这个解决方案持怀疑态度,因为尽管它似乎适用于所有主要编译器,但我已经读到它实际上是未定义的行为。如果其他人不熟悉这个技巧,这样的代码对于其他人阅读和理解也不是特别友好。
总而言之,有没有一种方法可以自动加载 64 位结构:
- 适用于 Clang 和 GCC,最好适用于大多数其他流行的编译器,
- 编译时生成一条指令,
- 不是未定义的行为,
- reader友善吗?
这个 clang 错过的优化只发生在 libstdc++ 上;正如我们对 -stdlib=libc++
所期望的那样,在 Godbolt 内联上发出 clang。 https://godbolt.org/z/Tt8XTX.
看来给struct 64位对齐就可以hand-hold clang了
libstdc++
的 std::atomic
模板针对自然对齐时小到足以成为原子的类型执行此操作,但也许 clang++ 只看到基础类型的对齐,而不是 class atomic<T>
的成员,在 libstdc++ 实现中。我没有调查过;有人应该将此报告给 clang / LLVM bugzilla。
#include <atomic>
#include <stdint.h> // you forgot this header.
struct A {
alignas(std::atomic_int64_t) int32_t x; // same alignment as std::atomic uses for atomic<int64_t>
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);
}
与 std::atomic<int64_t>
相同的对齐应该在每个目标上提供足够的对齐,其中 64 位对象完全可以是无锁的。 alignof(int64_t)
在 32 位 ABI 上可能只有 4,我没有使用 alignas(8)
来避免在 char 为 32 位且 sizeof(int64_t) = 的系统上过度对齐2.
alignas(2*sizeof(int32_t))
总是自然地对齐结构,即使在 atomic_int64_t 没有对齐的目标上,例如因为它不是无锁的。没关系。
Godbolt; clang13.0
仍然需要此解决方法# clang++ 9.0 -std=gnu++17 -O3; g++ is the same
f(std::atomic<A>&):
mov rax, qword ptr [rdi]
ret
顺便说一句,不,libatomic
库函数不会使用锁;它确实知道 8 字节对齐的加载自然是原子的,并且其他使用线程将使用普通 loads/stores,而不是锁。
较旧的 clang 至少使用 call __atomic_load_8
而不是通用的可变大小的,但这仍然是一个很大的优化失误。
有趣的事实:clang -m32
将使用 lock cmpxchg8b
来实现 8 字节原子加载,而不是像 GCC 那样使用 SSE 或 fild
。但这与使用 SSE 或 x87 是 ABI 兼容的,因此它不会被锁定到使用次优方式。 :/