让我们看看代码
#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
1条答案
按热度按时间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
时,我们也会收到接收地址的警告。__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
实现。