将指向原子类型的指针分配给指向非原子类型的指针
Posted
技术标签:
【中文标题】将指向原子类型的指针分配给指向非原子类型的指针【英文标题】:Assigning pointers to atomic type to pointers to non atomic type 【发布时间】:2019-08-28 01:08:41 【问题描述】:这段代码的行为是否明确?
#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
我(类型相同)。以下是我对这个例子的看法:
标准明确指定 const
、volatile
和 restrict
限定符与 _Atomic
限定符 6.2.5(p27)
的区别:
本标准明确使用“原子的、限定的或 不合格的类型”,只要允许类型的原子版本 以及类型的其他合格版本。词组 “合格或不合格类型”,没有具体提及原子, 不包括原子类型。
同时限定类型的兼容性被定义为6.7.3(p10)
:
为了使两个合格的类型兼容,两者都应具有 兼容类型的相同限定版本;的顺序 说明符或限定符列表中的类型限定符 不影响指定类型。
结合上面引用的引用,我得出结论,原子类型和非原子类型是兼容的类型。所以,应用简单分配6.5.16.1(p1)
(emp.mine)的规则:
左操作数有原子的、合格的或不合格的指针 类型,并且(考虑到左操作数的类型 左值转换后)两个操作数都是指向限定的指针 兼容类型的或不合格版本,以及指向的类型 左边有右边指向的类型的所有限定符;
所以我得出结论,行为是明确定义的(即使将原子类型分配给非原子类型)。
问题在于,应用上述规则,我们还可以得出结论,将非原子类型简单分配给原子类型也是很好定义的,即显然不是这样,因为我们有一个专用的通用 atomic_store
函数。
【问题讨论】:
【参考方案1】:6.2.5p27:
此外,还有 _Atomic 限定符。 _Atomic 的存在 限定符指定原子类型。大小、表示和 原子类型的对齐方式不必与原子类型的对齐方式相同 对应的不合格类型。因此,本标准明确 每当 允许类型的原子版本与其他合格的 类型的版本。短语''合格或不合格的类型'', 没有特别提到原子,不包括原子类型。
我认为这应该清楚地表明,原子限定类型不被认为与它们所基于的类型的限定或非限定版本兼容。
【讨论】:
【参考方案2】:C11 允许 _Atomic T
具有与 T
不同的大小和布局,例如如果它不是无锁的。 (见@PSkocik 的回答)。
例如,实现可以选择在每个原子对象中放置一个互斥体,并将其放在首位。 (大多数实现使用地址作为锁定表的索引:Where is the lock for a std::atomic?,而不是膨胀 _Atomic
或 std::atomic<T>
对象的每个实例,这在编译时不能保证无锁)。
因此_Atomic T*
与T*
不兼容,即使在单线程程序中也是如此。
仅仅分配一个指针可能不是 UB(对不起,我没有戴上我的语言律师帽子),但解引用当然可以。
我不确定在_Atomic T
和T
共享相同布局和对齐方式的实现上是否严格使用UB。如果 _Atomic T
和 T
被认为是不同的类型,无论它们是否共享相同的布局,它可能违反了严格的别名。
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 上,只是一个普通的 store。或者在弱序 ISA 上,一些栅栏但不是完整的屏障。)
对于无锁类型,_Atomic T
和T
的对象表示相同,在单线程程序中通过非原子指针访问原子对象在实践中没有问题 。不过,我怀疑它仍然是 UB。
C++20 计划引入std::atomic_ref<T>
,它可以让你对非原子变量进行原子操作。 (没有 UB,只要在写入的时间窗口内没有线程可能对其进行非原子访问。)这基本上是 GCC 中 __atomic_*
内置函数的包装器,例如,std::atomic<T>
是在顶部。
(这会带来一些问题,比如atomic<T>
需要比T
更多的对齐,例如对于i386 System V 上的long long
或double
。或者大多数64 位ISA 上的2x int
结构. 在声明您希望能够对其执行原子操作的非原子对象时,您应该使用alignas(_Atomic T) T foo
。)
无论如何,我不知道在可移植 ISO C11 中有任何符合标准的方式来执行类似的操作,但值得一提的是,真正的 C 编译器非常支持执行原子操作在没有_Atomic
声明的对象上。 但是only using stuff like GNU C atomic builtins.:
请参阅 Casting pointers to _Atomic pointers and _Atomic sizes :显然,即使在 GNU C 中也不建议将 T*
转换为 _Atomic T*
。尽管我们没有明确的答案,它实际上是 UB。
【讨论】:
您提到atomic_store(&my_atomic, 1)
等价于my_atomic=1;
。我尝试测试类似的东西并编写了以下函数:void do_test_atomic(volatile _Atomic int *ptr, int val) atomic_store(ptr, val);
编译与-O3
在函数末尾给出了mfence
。 godbolt.org/z/vrFCLT 。 (我对 intel x86 内存模型不太熟悉,所以如果我错了,请纠正我,但是 afaik 存储首先进入存储缓冲区,所以我们需要一个栅栏来避免由存储缓冲区转发引起的重新排序,我认为这可以解释 @ 987654384@).
@SomeName:是的,我在回答中说过。但是您没有尝试编译*ptr=val;
,这就是重点。 godbolt.org/z/OdxR_h 它编译成相同的程序集,mov+mfence
使用 gcc,或 a more efficient xchg [rdi], esi
) 使用 clang。在弱序 ISA 上,你会得到更多的围栏。或者 AArch64 有一个顺序发布存储的特殊指令......
“除非 GNU C 定义了将 T*
转换为 _Atomic T*
的行为”我在 2 周前问过这个问题。被告知使用内置函数:***.com/questions/55299525/…
@SomeName:不,我的意思是它在 C 抽象机中是等价的。 (另外,你可以在任何你想要的架构上用你想要的任何编译器检查它,x86 就是一个例子。)ISO C11“重载”赋值和_Atomic
类型的各种其他运算符。
@SomeName: 是的,这正是我在 C 抽象机中等同于 atomic_store
的意思。除了我很懒,没有在标准中查找确切的部分:P以上是关于将指向原子类型的指针分配给指向非原子类型的指针的主要内容,如果未能解决你的问题,请参考以下文章