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

Assigning pointers to atomic type to pointers to non atomic type

提问人:Some Name 提问时间:4/6/2019 最后编辑:Peter CordesSome Name 更新时间:4/9/2019 访问量:1153

问:

此代码的行为是否定义良好?

#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_Atomic6.2.5(p27)

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

此外,限定类型的兼容性定义为:6.7.3(p10)

对于要兼容的两种合格类型,两者都应具有 兼容类型的相同限定版本;的顺序 在说明符或限定符列表中键入限定符 不影响指定的类型。

结合上面引用的引文,我得出结论,原子类型和非原子类型是兼容的类型。因此,应用简单赋值规则(emp.mine):6.5.16.1(p1)

左操作数具有原子指针、限定指针或非限定指针 类型,并且(考虑到左操作数将具有的类型 左值转换后),这两个操作数都是指向兼容类型的限定或非限定版本的指针,并且指向 左边有右边所指类型的所有限定符;

因此,我得出结论,行为是明确定义的(即使将原子类型分配给非原子类型)。

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

C 并发 语言-律师 C11 标准

评论


答:

6赞 Peter Cordes 4/6/2019 #1

C11 允许具有与 不同的大小和布局,例如,如果它不是无锁的。(见@PSkocik的回答)。_Atomic TT

例如,实现可以选择在每个原子对象中放置一个互斥锁,并将其放在第一位。(大多数实现使用地址作为锁表的索引:std::atomic的锁在哪里?而不是在编译时不保证无锁的OR对象的每个实例膨胀)。_Atomicstd::atomic<T>

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

仅仅分配一个指针可能不是UB(对不起,我没有戴上我的语言律师帽子),但取消引用肯定是可以的。

我不确定它是否严格按照 UB 的方式实现,并且确实共享相同的布局和对齐方式。它可能违反了严格的别名,如果 和 被视为不同的类型,无论它们是否共享相同的布局。_Atomic TT_Atomic TT


alignof(T) 可能与 alignof(_Atomic T) 不同,但除了故意反常的实现 (Deathstation 9000) 之外,它至少与 plain 一样对齐,因此这不是将指针投射到已经存在的对象的问题。一个比它需要的更对齐的对象不是问题,只是如果它阻止编译器使用单个更广泛的负载,则可能错过优化。_Atomic TT

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


在实践中,在实际实现上,并使用相同的布局/对象表示和.如果可以解决严格别名的 UB,则程序的单线程或互斥保护部分可以对对象进行非原子访问。也许用._Atomic T*T*alignof(_Atomic T) >= alignof(T)_Atomicmemcpy

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


所有这一切的问题在于,应用上述规则,我们还可以得出结论,将非原子类型简单赋值给原子类型也是明确定义的,这显然不是真的,因为我们有一个专用的泛型atomic_store函数。

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

atomic_store(&my_atomic, 1)等同于 。在 C 抽象机器中,它们都使用 .my_atomic=1;memory_order_seq_cst

您还可以通过查看任何 ISA 上实际编译器的代码生成来了解这一点;例如,x86 编译器将使用指令或 +。同样,编译为原子 RMW(使用 )。xchgmovmfenceshared_var++mo_seq_cst

IDK 为什么有一个泛型函数。也许只是为了与atomic_store_explicit形成对比/一致性,这让你做或做一个释放或放松的存储,而不是顺序释放。(在 x86 上,只是一个普通的商店。或者在弱序ISA上,一些围栏但不是完整的障碍。atomic_storeatomic_store_explicit(&shared_var, 1, memory_order_release)memory_order_relaxed


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

C++20 计划引入 std::atomic_ref<T>这将允许您对非原子变量进行原子运算。(没有 UB,只要没有线程在写入的时间窗口内可能对它进行非原子访问。这基本上是 GCC 中内置函数的包装器,例如,它是在 GCC 之上实现的。__atomic_*std::atomic<T>

(这带来了一些问题,例如是否需要比 更多的对齐,例如,用于或用于 i386 System V。或者在大多数 64 位 ISA 上为 2x 结构。在声明希望能够执行原子操作的非原子对象时,应使用。atomic<T>Tlong longdoubleintalignas(_Atomic T) T foo

无论如何,我不知道在可移植的 ISO C11 中有任何符合标准的方法可以做类似的事情,但值得一提的是,真正的 C 编译器确实非常支持对没有_Atomic声明的对象执行原子操作。只使用像 GNU C 原子内置的东西。

参见 将指针转换为_Atomic指针和_Atomic大小:显然,即使在 GNU C 中也不建议将指针转换为。虽然我们没有一个明确的答案,它实际上是UB。T*_Atomic T*

评论

0赞 Some Name 4/6/2019
您提到 atomic_store(&my_atomic, 1) 等价于 my_atomic=1;。我尝试测试类似的东西并编写了以下函数:编译时在函数的末尾给出了。godbolt.org/z/vrFCLT .(我对 intel x86 内存模型不太熟悉,所以如果我错了,请纠正我,但 afaik 存储首先进入存储缓冲区,所以我们需要一个围栏来避免由存储缓冲区转发引起的重新排序,我认为这解释了 )。void do_test_atomic(volatile _Atomic int *ptr, int val){ atomic_store(ptr, val); }-O3mfencemfence
1赞 Peter Cordes 4/6/2019
@SomeName:是的,我在回答中是这么说的。但是你没有尝试编译,这就是重点。godbolt.org/z/OdxR_h它编译为相同的汇编,使用 gcc,或者使用 clang 编译为更高效的 xchg [rdi], esi)。在弱序 ISA 上,你会得到更多的围栏。或者 AArch64 对 seqeuential-release 存储有特殊说明......*ptr=val;mov+mfence
2赞 Petr Skocik 4/6/2019
“除非 GNU C 定义了将 a 转换为的行为”,我在 2 周前问过这个问题。被告知使用内置函数:stackoverflow.com/questions/55299525/...T*_Atomic T*
1赞 Peter Cordes 4/6/2019
@SomeName:不,我的意思是它在 C 抽象机器中是等价的。(另外,你可以在任何你想要的架构上使用任何你想要的编译器来检查它,x86 就是一个例子。ISO C11“重载”赋值和各种其他类型的运算符。_Atomic
1赞 Peter Cordes 4/16/2019
@SomeName: yes, that's exactly what I meant about it being equivalent to in the C abstract machine. Except I was lazy and didn't look up the exact section in the standard :Patomic_store
8赞 Petr Skocik 4/6/2019 #2

6.2.5p27:

Further, there is the _Atomic qualifier. The presence of the _Atomic qualifier designates an atomic type. The size, representation, and alignment of an atomic type need not be the same as those of the corresponding unqualified type. Therefore, this Standard explicitly uses the phrase ''atomic, qualified or unqualified type'' whenever the atomic version of a type is permitted along with the other qualified versions of a type. The phrase ''qualified or unqualified type'', without specific mention of atomic, does not include the atomic types.

I think this should make it clear that atomic-qualified types are not deemed compatible with qualified or unqualified versions of the types they're based on.