将原子类型的指针分配给非原子类型的指针

时间:2019-04-06 08:03:35

标签: c concurrency language-lawyer c11 stdatomic

此代码的行为是否定义明确?

#include <stdatomic.h>

const int test = 42;
const int * _Atomic atomic_int_ptr;
atomic_init(&atomic_int_ptr, &test);
const int ** int_ptr_ptr = &atomic_int_ptr;
printf("int = %d\n", **int_ptr_ptr); //prints int = 42

我将原子类型的指针分配给非原子类型的指针(类型相同)。这是我对这个示例的看法:

该标准明确规定了constvolatilerestrict限定词与_Atomic限定词6.2.5(p27)的区别:

  

本标准明确使用短语“原子的,合格的或   非限定类型”,只要允许使用该类型的原子版本   以及其他类型的合格版本。词组   “'合格或不合格类型'”,但没有具体提及原子,   不包括原子类型。

还将限定类型的兼容性定义为6.7.3(p10)

  

要使两个合格的类型兼容,两个都应具有   兼容类型的完全相同的版本;的顺序   限定符或限定符列表中的类型限定符   不影响指定的类型。

结合以上引用,我得出结论,原子类型和非原子类型是兼容的类型。因此,应用简单分配规则6.5.16.1(p1)(临时矿):

  

左操作数具有原子,合格或不合格的指针   类型,以及(考虑到左操作数将具有的类型   左值转换后)两个操作数都是限定 的指针   兼容类型的 或不合格版本,以及由   左侧具有右侧所指类型的所有限定词;

因此,我得出的结论是,行为已得到很好的定义(即使将原子类型分配为非原子类型也是如此)。

所有问题是应用上述规则,我们还可以得出结论: 将非原子类型简单分配给原子类型 显然不正确,因为我们有专用的泛型atomic_store函数。

2 个答案:

答案 0 :(得分:8)

6.2.5p27

  

此外,还有_Atomic限定词。 _Atomic的存在   限定词指定原子类型。大小,表示形式和   原子类型的对齐方式不必与原子类型的对齐方式相同   对应的不合格类型。因此,本标准明确   无论何时,只要使用“原子,合格或不合格类型”一词,   一个类型的原子版本与其他合格版本一起被允许   类型的版本。短语“合格或不合格类型”,   没有具体提及原子,不包括原子类型。

我认为这应该明确表明,原子限定的类型与它们所基于的类型的限定或不限定版本兼容。

答案 1 :(得分:6)

C11允许_Atomic T具有与T不同的大小和布局,例如如果它不是无锁的。 (请参阅@PSkocik的答案)。

例如,实现可以选择将互斥锁放在每个原子对象中,然后将其放在最前面。 (大多数实现将地址用作锁表的索引:Where is the lock for a std::atomic?而不是膨胀_Atomicstd::atomic<T>对象的每个实例,这些对象不能保证在编译时没有锁时间)。

因此,即使在单线程程序中,_Atomic T*也与T*不兼容。

仅分配一个指针可能不是UB (很抱歉,我没有戴上语言律师的帽子),但取消引用当然可以

我不确定_Atomic TT确实共享相同的布局和对齐方式的实施是否严格意义上的UB。如果_Atomic TT被认为是不同的类型,而不论它们是否共享相同的布局,则可能会违反严格的别名。


alignof(T)可能与alignof(_Atomic T) 不同,但除了故意的不正当实施方式(Deathstation 9000)以外,_Atomic T至少与普通对齐T,因此将指针转换为已经存在的对象不是问题。对齐程度比实际需要的对象不是问题,如果它阻止编译器使用单个更宽的负载,则可能是未命中的优化。

有趣的事实:在ISO C中,即使未取消引用,创建未对齐的指针也是UB。 (大多数实现都不会抱怨,英特尔的_mm_loadu_si128内在函数甚至需要编译器来支持。)


在实际实现中,_Atomic T*T*使用相同的布局/对象表示形式和alignof(_Atomic T) >= alignof(T)。如果可以解决严格混淆的UB,则程序的单线程或互斥锁保护的部分可以对_Atomic对象进行非原子访问。也许和memcpy在一起。

在实际的实现中,_Atomic可能会增加对齐要求,例如对于大多数64位ISA,大多数ABI上的struct {int a,b;}通常仅具有4字节对齐方式(最多成员数),但是_Atomic将使其自然对齐方式= 8以允许使用以下方式加载/存储它单个对齐的64位加载/存储。当然,这不会改变成员相对于对象起点的布局或对齐方式,而只会更改整个对象的对齐方式。


  

所有问题的是,应用上述规则,我们还可以得出结论:将非原子类型简单分配给原子类型也定义得很好,这显然是不正确的,因为我们有专门的泛型atomic_store函数。

不,这种推理是有缺陷的。

atomic_store(&my_atomic, 1)等效于my_atomic=1;。在C抽象机中,它们都使用memory_order_seq_cst进行原子存储。

通过查看任何ISA上实际编译器的代码源,您还可以看到这一点;例如x86编译器将使用xchg指令或mov + mfence。同样,shared_var++编译为原子RMW(带有mo_seq_cst)。

IDK为什么会有atomic_store泛型函数。也许只是为了与atomic_store_explicit形成对比/一致性,它使您可以atomic_store_explicit(&shared_var, 1, memory_order_release)memory_order_relaxed进行发布或放松存储,而不是顺序发布。 (在x86上,这只是一个普通的存储。或者在顺序较弱的ISA上,有一定的栅栏,但没有完整的屏障。)


对于无锁类型,_Atomic TT的对象表示相同,在实践中通过非原子指针访问原子对象没有问题。单线程程序。我怀疑它仍然是UB。

C ++ 20计划引入std::atomic_ref<T>,它将使您可以对非原子变量执行原子操作。 (没有UB,只要在编写的时间段内没有线程潜在地对其进行非原子访问。)例如,这基本上是围绕GCC中__atomic_*内置函数的包装,例如,{{1} }的实现。

(这会带来一些问题,例如如果std::atomic<T>atomic<T>需要更多的对齐方式,例如i386系统V上的Tlong long。或2x { {1}}在大多数64位ISA上。在声明要对其执行原子操作的非原子对象时,应使用double

无论如何,我不知道在便携式 ISO C11中执行类似操作的任何符合标准的方法,但是值得一提的是,真正的C编译器确实支持原子操作声明没有int的对象上。但是only using stuff like GNU C atomic builtins.

请参见Casting pointers to _Atomic pointers and _Atomic sizes:显然,即使在GNU C中,也不建议将alignas(_Atomic T) T foo强制转换为_Atomic。尽管我们没有确切的答案,实际上它是UB。