C语言 为什么跨缓存行边界的变量的原子存储编译为正常的MOV存储指令?

2ledvvac  于 2023-04-19  发布在  其他
关注(0)|答案(1)|浏览(124)

让我们看看代码

#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
inn6fuwd

inn6fuwd1#

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

您创建了一个未对齐的uint64_t并获取了它的地址。这就是not safe in general。打包的结构体只有在您直接通过结构体访问其未对齐的成员时才能可靠地工作。您也可以使用未定义的未对齐指针行为create crashes,例如使用打包的struct { char a; int arr[1024]; },然后将指针作为普通的int*传递给可能自动向量化的函数。
如果你在没有充分对齐的变量上使用__atomic_store_n,这是未定义的行为AFAIK。我不认为它支持typedef __attribute__((aligned(1), may_alias)) int *unaligned_int;产生不同的asm。
GCC's __atomic builtins无法像alignas(std::atomic_ref<uint64_t>::required_alignment) uint64_t foo;那样查询所需的对齐
有一个bool __atomic_is_lock_free (size_t size, void *ptr),它接受一个指针arg来检查对齐情况(0表示类型的典型/默认对齐方式),但如果size=8,即使使用guaranteed-cache-line-split对象(如_Alignas(64) test global_t;a8成员),它也会返回1。(如果结构的开始没有已知的对齐方式,指向对象中的a8可能恰好完全在一个缓存行中,这在Intel上足够了,但在AMD上不能保证原子性。

**我认为你应该假设对于任何无锁原子,它需要alignas(sizeof(T)),即自然对齐,否则你不能安全地在上面使用__atomic内置。

另见atomic_ref when external underlying type is not aligned as requested re:这种情况下实现设计考虑,是否检查对齐并使事情变慢,或者是否让用户像您一样通过使访问非原子化来搬起石头砸自己的脚。
GCC可以检测到这一点并发出警告,这很好,但我不希望他们为x86的未对齐原子访问能力添加编译器后端支持(RMW指令的前缀为lock,或xchg),代价是性能极差,会锁定总线,从而降低其他内核的速度。这对现代众核服务器来说是一场灾难。所以没人想这样,正确的解决方法是修改代码。
大多数其他ISA根本不能执行未对齐的原子操作。
半相关:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4-即使在非压缩结构中,GCC也会长时间对C11 _Atomic成员进行欠对齐,例如,保持默认的alignof(uint64_t)==4在一些32位ISA上,如x86 -m32,不升级到必要的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++ atomic mappings to x86 asm将阻止seq_cst操作之间的StoreLoad重新排序的成本放在了store上,使SC加载与获取相同。)因此,对于已知未对齐的8字节加载,clang选择不内联__atomic_load_n不会获得任何好处。
OTOH它不会伤害,并且libatomic的自定义实现可以做一些事情,例如lock cmpxchg,或者如果你在一些模拟器或其他奇怪的环境中运行的话。
有趣的是,clang基于未对齐选择不内联。但它的警告只对x86-64上的原子RMW操作有意义,在那里它是性能损失而不是缺乏原子性。或者SC存储,只要libatomic使用xchg而不是mov + mfence实现。

相关问题