c -为什么跨缓存线边界的原子存储被编译成普通的MOV存储指令?



让我们看一下代码

#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

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

您创建了一个未对齐的uint64_t并获取了其地址。一般来说这是不安全的。打包结构只有在直接通过结构访问其不对齐的成员时才能可靠地工作。你也可以使用指针不对齐的未定义行为造成崩溃,例如,使用打包的struct { char a; int arr[1024]; },然后将指针作为普通的int*传递给可能自动向量化的函数。

如果你在没有充分对齐的变量上使用__atomic_store_n,这是未定义的行为。我认为它不支持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),它接受一个指针参数来检查对齐(0为典型/默认的类型对齐),但它返回1为size=8,即使有一个像_Alignas(64) test global_t;a8成员的guaranteed-cache-line-split对象。(没有已知的结构体开始对齐,指向对象中的a8可能恰好完全在一条缓存行内,这在Intel上是足够的,但在AMD上不能保证原子性。)

我认为你应该假设任何无锁原子,它需要alignas(sizeof(T)),即自然对齐,否则你不能安全地使用__atomic内置在它上面。这在https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html中没有明确记录,但可能在其他地方。


参见atomic_ref,当外部底层类型没有按照要求对齐时。re:实现设计考虑这种情况,是否检查对齐并使事情变慢,或者是否让用户像你一样做非原子访问,让他们搬起石头砸自己的脚。

GCC可以检测到这一点并发出警告,这将是很好的,但我不希望他们添加编译器后端支持x86进行不对齐原子访问的能力(用lock前缀表示RMW指令,或xchg),代价是性能非常差,这会锁定总线,从而降低其他内核的速度。这在现代多核服务器上是一场灾难,所以没有人想要这样,正确的解决办法是修复你的代码。

大多数isa根本不能做不对齐的原子操作。


半相关:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4 -即使在非打包结构中,GCC在很长一段时间内也没有对齐C11_Atomic成员,例如在一些32位ISAs上保持默认的对齐(uint64_t)==4,如x86-m32,而不是提升到必要的alignas(sizeof(T))_Atomic uint64_t a8不会改变GCC的代码生成,即使是直接加载,clang也拒绝编译它。

有趣的clang输出

正如您注意到的,它警告,不像GCC。在结构体上使用__attribute__((packed))而不是#pragma pack时,我们也会收到获取地址的警告。(Godbolt)

<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的原子映射将阻止StoreLoad在seq_cst操作之间重新排序的成本放在了store上,使得SC加载与acquire相同。)所以clang没有得到任何东西,选择不内联__atomic_load_n为一个已知不对齐的8字节加载。

OTOH它没有伤害,libatomic的自定义实现可以做一些事情,例如lock cmpxchg,或者其他任何如果你在一些模拟器或其他奇怪的环境中运行。

有趣的是,clang基于不对齐选择不内联。但是它的警告只对x86-64上的原子RMW操作有意义,在这种情况下,它是一种性能损失,而不是缺乏原子性。或者SC store,只要libatonic用xchg而不是mov+mfence来实现。

相关内容

  • 没有找到相关文章

最新更新