为什么跨高速缓存行边界的变量原子存储编译为普通 MOV 存储指令?

问题描述 投票:0回答:1

让我们看一下代码

#include <stdint.h>
#pragma pack (push,1)
typedef struct test_s
{
    uint64_t a1;
    uint64_t a2;
    uint64_t a3;
    uint64_t a4;
    uint64_t a5;
    uint64_t a6;
    uint64_t a7;
    uint8_t b1;
    uint64_t a8;
}test;

int main()
{
    test t;
    __atomic_store_n(&(t.a8), 1, __ATOMIC_RELAXED);
}

因为我们有打包结构,a8 不是自然对齐的,也应该在不同的 64 字节缓存边界之间拆分,但是生成的程序集 GCC 12.2 是

main:
        push    rbp
        mov     rbp, rsp
        mov     eax, 1
        mov     QWORD PTR [rbp-23], rax
        mov     eax, 0
        pop     rbp
        ret

为什么翻译成简单的MOV?在那种情况下,MOV 不是原子的吗?

补充: clang 16 上的相同代码调用原子函数并转换为

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        sub     rsp, 80
        lea     rdi, [rbp - 72]
        add     rdi, 57
        mov     qword ptr [rbp - 80], 1
        mov     rsi, qword ptr [rbp - 80]
        xor     edx, edx
        call    __atomic_store_8@PLT
        xor     eax, eax
        add     rsp, 80
        pop     rbp
        ret
c gcc x86 atomic memory-alignment
1个回答
3
投票

正确,在这种情况下存储不是原子的,GNU C 不支持未对齐的原子操作。

您创建了一个未对齐的

uint64_t
并获取了它的地址。那是一般不安全。打包结构只有在您直接通过结构访问未对齐的成员时才能可靠地工作。您还可以 create crashes with misaligned-pointer undefined behavior,例如使用打包的
struct { char a; int arr[1024]; }
,然后将指针作为普通的
int*
传递给可能自动矢量化的函数。

如果您在未充分对齐的变量上使用

__atomic_store_n
,这是未定义的行为 AFAIK。我不认为它支持
typedef __attribute__((aligned(1), may_alias)) int *unaligned_int;
生产不同的 asm.

GCC 的

__atomic
内置函数 没有办法查询所需的对齐方式,就像我们可以使用
alignas(std::atomic_ref<uint64_t>::required_alignment) uint64_t foo;

bool __atomic_is_lock_free (size_t size, void *ptr)
需要一个指针 arg 来检查对齐(
0
对于类型的典型/默认对齐),但它返回
1
for size=8 即使有一个 guaranteed-cache-line-split object就像
a8
_Alignas(64) test global_t;
成员。 (如果结构的开头没有已知的对齐方式,指向对象中的
a8
可能恰好完全在一个缓存行内,这对于英特尔来说足够了,但对于原子性保证来说还不够AMD。)

我认为你应该假设对于任何无锁原子,它需要

alignas(sizeof(T))
,即自然对齐,否则你不能安全地使用
__atomic
内置函数。
这在中没有明确记录https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html 但也许在别的地方。


另请参阅atomic_ref 当外部底层类型未按要求对齐时 回复:该案例的实现设计注意事项,是否检查对齐并使事情变慢,或者是否让用户像你一样搬起石头砸自己的脚,通过使访问成为非原子的。

GCC 可以检测到这一点并发出警告,这很好,但我不希望他们为 x86 进行未对齐原子访问的能力添加编译器后端支持(RMW 指令带有

lock
前缀,或
xchg 
) 以极差的性能为代价,它会锁定总线,从而减慢其他内核的速度。这对现代多核服务器来说是一场灾难,所以没有人想要这样,正确的解决办法是修复你的代码。

大多数其他 ISA 根本无法执行未对齐的原子操作。


半相关:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4 - 即使在非压缩结构中,GCC 也长期未对齐 C11

_Atomic
成员,例如在某些 32 位 ISA(如 x86
-m32
)上保持默认的 alignof(uint64_t)==4,而不是提升到必要的
alignas(sizeof(T))
_Atomic uint64_t a8
不会更改 GCC 的代码生成,即使直接直接加载,clang 也拒绝编译它。

有趣的 clang 输出

正如您所注意到的,它会发出警告,这与 GCC 不同。在结构上使用

__attribute__((packed))
而不是
#pragma pack
,我们还会收到获取地址的警告。 (神箭)

<source>:41:30: warning: taking address of packed member 'a8' of class or structure 'test_s' may result in an unaligned pointer value [-Waddress-of-packed-member]
    return __atomic_load_n(&(t->a8), __ATOMIC_RELAXED);
                             ^~~~~
<source>:41:12: warning: misaligned atomic operation may incur significant performance penalty; the expected alignment (8 bytes) exceeds the actual alignment (1 bytes) [-Watomic-alignment]
    return __atomic_load_n(&(t->a8), __ATOMIC_RELAXED);

__atomic_store_8
库函数 clang 调用实际上会在 x86-64 上提供原子性;它忽略了 RDX 中的 memory_order 参数并假设
__ATOMIC_SEQ_CST
- 实现只是
xchg [rdi],rsi
/
ret
.

但是

__atomic_load_8
不会:它的实现是
mov rax, [rdi]
/
ret
(因为 C++ 原子映射到 x86 asm 将阻止 seq_cst 操作之间的 StoreLoad 重新排序的成本放到存储上,使 SC 加载与获取相同。 )因此,通过选择不内联
__atomic_load_n
对于已知未对齐的 8 字节加载,clang 并没有获得任何好处。

OTOH 它没有伤害,并且 libatomic 的自定义实现可以对此做一些事情,例如使用

lock cmpxchg
,或者如果您在某些模拟器或其他奇怪的环境中运行,则使用其他任何东西。

有趣的是 clang 根据未对齐选择不内联。但它的警告只对 x86-64 上的原子 RMW 操作有意义,它是性能损失而不是缺乏原子性。或 SC 存储,只要 libatomic 使用

xchg
而不是
mov
+
mfence
实现。

© www.soinside.com 2019 - 2024. All rights reserved.