繁体   English   中英

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()

这对我来说是有问题的,原因有几个:

  1. 更明显的是,指令要多得多,所以我希望代码效率较低
  2. 不太明显,请注意生成的代码还包括对库函数__atomic_load的调用,这意味着我的二进制文件需要与 libatomic 链接。 这意味着我需要不同的库列表来链接,具体取决于我的代码用户是使用 GCC 还是 Clang。
  3. 库函数可能会使用锁,这会降低性能

我现在想到的重要问题是是否有办法让 Clang 也将负载转换为单个指令。 我们将它用作我们计划分发给其他人的库的一部分,因此我们不能依赖正在使用的特定编译器。 到目前为止向我建议的解决方案是使用类型双关并将结构体与 64 位整数一起存储在联合中,因为 Clang 确实在一条指令中正确地以原子方式加载了 64 位整数。 然而,我对这个解决方案持怀疑态度,因为虽然它似乎适用于所有主要编译器,但我已经读到它实际上是未定义的行为。 如果其他人不熟悉该技巧,则此类代码对其阅读和理解也不是特别友好。

总而言之,有没有办法自动加载 64 位结构:

  1. 适用于 Clang 和 GCC,最好是大多数其他流行的编译器,
  2. 编译时生成一条指令,
  3. 不是未定义的行为,
  4. 读者友好吗?

这种铿锵的优化只发生在 libstdc++ 中; 正如我们对-stdlib=libc++所期望的那样,在 Godbolt 内联上叮当-stdlib=libc++ https://godbolt.org/z/Tt8XTX

似乎给 struct 64 位对齐足以手持 clang。

libstdc++std::atomic模板对自然对齐时足够小以成为原子的类型执行此操作,但也许 clang++ 在 libstdc++ 实现中只看到底层类型的对齐,而不是atomic<T>的类成员. 我没有调查过; 有人应该将此报告给 clang / LLVM bugzilla。

#include <atomic>
#include <stdint.h>  // you forgot this header.

struct A {
    alignas(2 * sizeof(int32_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);
}

按结构大小对齐使其与alignof(int64_t)无关,在 32 位 ABI 上可能只有 4。(我没有使用alignas(8)来避免在 char 为 32 位的系统上过度对齐和 sizeof(int64_t) = 2.) 这可能是不必要的复杂,而alignas(int64_t)更容易阅读,即使它并不总是与给这个结构自然对齐相同的东西。)

神箭

# 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而不是通用的可变大小的call __atomic_load_8 ,但这仍然是一个很大的遗漏优化。

有趣的事实: clang -m32将使用lock cmpxchg8b来实现 8 字节的原子加载,而不是像 GCC 那样使用 SSE 或fild :/

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM