为啥 std::atomic<T>::is_lock_free() 不像 constexpr 一样是静态的?

Posted

技术标签:

【中文标题】为啥 std::atomic<T>::is_lock_free() 不像 constexpr 一样是静态的?【英文标题】:Why is std::atomic<T>::is_lock_free() not static as well as constexpr?为什么 std::atomic<T>::is_lock_free() 不像 constexpr 一样是静态的? 【发布时间】:2020-03-08 00:25:45 【问题描述】:

谁能告诉我std::atomic&lt;T&gt;::is_lock_free() 是否不像 constexpr 那样是静态的?让它非静态和/或非 constexpr 对我来说没有意义。

一开始为什么不设计成C++17's is_always_lock_free

【问题讨论】:

你知道is_always_lock_free吗? 我要把“对齐”扔出去。 @MaxLanghof 你的意思是不是所有的实例都会以同样的方式对齐? 迈克,不,我不知道,但感谢您的提示;这对我很有帮助。但我在问自己为什么要在 is_lock_free() 和 is_always_lock_free 之间做出决定。这不可能是因为未对齐的原子,其他人在这里建议,因为该语言将未对齐的访问定义为具有未定义的行为。 【参考方案1】:

如cppreference 所述:

除了 std::atomic_flag 之外的所有原子类型都可以使用互斥锁或其他锁定操作来实现,而不是使用无锁原子 CPU 指令。 原子类型有时也可以是无锁的,例如如果在给定架构上只有对齐的内存访问自然是原子的,那么相同类型的未对齐对象必须使用锁。

C++ 标准推荐(但不要求)无锁原子操作也是无地址的,即适用于使用共享内存的进程之间的通信。

正如多位其他人所提到的,std::is_always_lock_free 可能是您真正想要的。


编辑:澄清一下,C++ 对象类型有一个对齐值,将其实例的地址限制为仅是 2 的某些幂的倍数 ([basic.align])。这些对齐值是针对基本类型的实现定义的,并且不需要等于类型的大小。它们也可能比硬件实际支持的更严格。

例如,x86(大部分)支持非对齐访问。但是,您会发现大多数编译器都有 alignof(double) == sizeof(double) == 8 for x86,因为未对齐的访问有许多缺点(速度、缓存、原子性......)。但是例如#pragma pack(1) struct X char a; double b; ;alignas(1) double x; 允许您“未对齐”doubles。因此,当 cppreference 谈到“对齐的内存访问”时,它大概是根据硬件类型的自然对齐来这样做的,而不是以与其对齐要求相矛盾的方式使用 C++ 类型(这将是 UB)。

这里有更多信息:What's the actual effect of successful unaligned accesses on x86?

还请查看下面 @Peter Cordes 提供的富有洞察力的 cmets!

【讨论】:

32 位 x86 是一个很好的例子,说明在哪里可以找到带有 alignof(double)==4 的 ABI。但是std::atomic&lt;double&gt; 仍然有alignof() = 8,而不是在运行时检查对齐。使用欠对齐 atomic 的打包结构会破坏 ABI,并且不受支持。 (用于 32 位 x86 的 GCC 更喜欢给 8 字节对象自然对齐,但结构打包规则会覆盖它并且仅基于 alignof(T),例如在 i386 System V 上。G++ 曾经有一个错误,其中 atomic&lt;int64_t&gt; 内部一个结构可能不是原子的,因为它只是假设。GCC(对于 C 而不是 C++)仍然有这个错误!) 但是 C++20 std::atomic_ref&lt;double&gt; 的正确实现将要么完全拒绝未对齐的 double,要么将在运行时在纯 double 和 @987654341 合法的平台上检查对齐@ 小于自然对齐。 (因为atomic_ref&lt;T&gt; 对声明为普通T 的对象进行操作,并且只有alignof(T) 的最小对齐,而没有机会给它额外的对齐。) 请参阅gcc.gnu.org/bugzilla/show_bug.cgi?id=62259 了解现在已修复的 libstdc++ 错误,gcc.gnu.org/bugzilla/show_bug.cgi?id=65146 了解仍未解决的 C 错误,包括一个纯 ISO C11 测试用例,显示使用当前编译时 _Atomic int64_t 撕裂gcc -m32。无论如何,我的观点是真正的编译器不支持对齐不足的原子,并且不进行运行时检查(还没有?),所以 #pragma pack__attribute__((packed)) 只会导致非原子性;对象仍会报告它们是lock_free 但是是的,is_lock_free() 的目的是允许实现与当前实现的实际工作方式不同;使用基于实际对齐的运行时检查以使用硬件支持的原子指令或使用锁。【参考方案2】:

您可以使用std::is_always_lock_free

is_lock_free取决于实际系统,无法在编译时确定。

相关说明:

有时也允许原子类型是无锁的,例如要是 对齐的内存访问在给定的架构上自然是原子的, 相同类型的未对齐对象必须使用锁。

【讨论】:

std::numeric_limits&lt;int&gt;::max 取决于架构,但它是静态的,constexpr。我猜答案没有错,但我不买推理的第一部分 无论如何都没有将未对齐的语言定义为具有未定义的行为,因此在运行时评估无锁性与否将是无稽之谈? 在对齐访问和非对齐访问之间做出决定是没有意义的,因为语言将后者定义为未定义的行为。 @BonitaMontero 有“在 C++ 对象对齐中未对齐”的含义和“在硬件喜欢的情况下未对齐”的含义。这些不一定相同,但在实践中它们经常是相同的。您展示的示例是一个这样的实例,其中编译器显然具有两个 相同的内置假设 - 这仅意味着 is_lock_free 在该编译器上毫无意义 . 如果有对齐要求,您可以非常确定原子将具有正确的对齐方式。【参考方案3】:

我已经在我的 Windows-PC 上安装了 Visual Studio 2019,这个 devenv 还有一个 ARMv8 编译器。 ARMv8 允许未对齐的访问,但比较和交换、锁定添加等都必须对齐。此外,使用ldpstp(32 位寄存器的加载对或存储对)的纯加载/纯存储仅在它们自然对齐时才能保证是原子的。

所以我写了一个小程序来检查 is_lock_free() 为任意原子指针返回什么。代码如下:

#include <atomic>
#include <cstddef>

using namespace std;

bool isLockFreeAtomic( atomic<uint64_t> *a64 )

    return a64->is_lock_free();

这是isLockFreeAtomic的反汇编

|?isLockFreeAtomic@@YA_NPAU?$atomic@_K@std@@@Z| PROC
    movs        r0,#1
    bx          lr
ENDP

这只是returns true,又名1

此实现选择使用alignof( atomic&lt;int64_t&gt; ) == 8,因此每个atomic&lt;int64_t&gt; 都正确对齐。这避免了每次加载和存储时都需要进行运行时对齐检查。

(编者注:这很常见;大多数现实生活中的 C++ 实现都是这样工作的。这就是 std::is_always_lock_free 如此有用的原因:因为它通常适用于 is_lock_free() 永远为真的类型。)

【讨论】:

是的,大多数实现选择提供atomic&lt;uint64_t&gt;alignof() == 8,因此它们不必在运行时检查对齐。这个旧 API 为他们提供了不这样做的选项,但在当前的硬件上,只需要对齐(否则 UB,例如非原子性)就更有意义了。即使在 int64_t 可能只有 4 字节对齐的 32 位代码中,atomic&lt;int64_t&gt; 也需要 8 字节。见my comments on another answer 换个说法:如果编译器选择使基本类型的alignof值与硬件的“良好”对齐相同,那么 is_lock_free 将永远是 trueis_always_lock_free 也是如此)。您的编译器正是这样做的。但是 API 存在,所以其他编译器可以做不同的事情。 您可以非常确定,如果语言说未对齐访问具有未定义的行为,则所有原子都必须正确对齐。因此,任何实现都不会进行任何运行时检查。 @BonitaMontero 是的,但是语言中没有任何内容禁止alignof(std::atomic&lt;double&gt;) == 1(因此在 C++ 意义上不会存在“未对齐访问”,因此没有 UB),即使硬件只能保证doubles 在 4 或 8 字节边界上的无锁原子操作。然后编译器必须在未对齐的情况下使用锁(并从 is_lock_free 返回适当的布尔值,具体取决于对象实例的内存位置)。 @MaxLanghof:是的,非静态std::atomic&lt;&gt;::is_lock_free() API 旨在允许选择实现。对于现实世界的实现来说,这将是一个糟糕的选择,因此它们实际上不是这样工作的。在比 alignof 对齐更少的 std::atomic&lt;&gt; 对象上调用它已经是 UB,因此它仍然返回 true 的事实并不违反任何规定,只是意味着 API 对检测该问题没有帮助。跨度> 【参考方案4】:

std::atomic&lt;T&gt;::is_lock_free() 可能在某些实现中返回truefalse,具体取决于运行时条件。

正如 Peter Cordes 在 cmets 中指出的那样,运行时条件不是对齐,因为原子会(过度)对齐内部存储以实现高效的无锁操作,而强制不对齐是 UB,可能表现为原子性的丧失。

可以创建一个不强制对齐并基于对齐进行运行时调度的实现,但这不是一个理智的实现会做的事情。只有在 __STDCPP_DEFAULT_NEW_ALIGNMENT__ 小于所需的原子对齐时,支持 C++17 之前的版本才有意义,因为动态分配的过度对齐在 C++17 之前不起作用。

运行时条件可能决定原子性的另一个原因是运行时 CPU 调度

在 x86-64 上,实现可以在初始化时通过 cpuid 检测 cmpxchg16b 的存在,并将其用于 128 位原子,同样适用于 cmpxchg8b 和 32 位上的 64 位原子.如果找不到对应的cmpxchg,则无锁原子无法实现,实现使用锁。

MSVC 当前不执行运行时 CPU 调度。由于 ABI 兼容性原因,它不会为 64 位执行此操作,并且不会为 32 位执行此操作,因为它已经不支持没有 cmpxchg8b 的 CPU。 Boost.Atomic 默认不这样做(假设cmpxchg8bcmpxhg16b 存在),但可以配置用于检测。我还没有费心去看看其他实现是做什么的。

【讨论】:

非静态std::atomic&lt;&gt;::is_lock_free() API 确实允许实现alignof(std::atomic&lt;T&gt;) 小于sizeof 的可能性。当前实现选择具有 alignof == sizeof,因此它们不需要运行时对齐检查。 (这意味着在未对齐的 atomic&lt;T&gt; 对象上调用 is_lock_free 或任何其他成员函数是 UB,因此返回值是什么并不重要。)无论如何,这是一个实现选择,而不是 ISO C+ 的约束+11。 (不过,这是一个很好的实现选择!)不过,关于运行时调度的另一个原因是好的。 @PeterCordes,是的,已更正。在另一个想法上,我发现了一个不依赖对齐的可能原因:在 C++17 之前,new 的对齐固定为__STDCPP_DEFAULT_NEW_ALIGNMENT__,并且不能增加alignas。我不认为某些实现使用比最大无锁原子所需的更小的分配对齐,但它看起来是提供标准方法来处理这个问题的理由。 关于new 的有趣点。您可以考虑对最大对象大小进行运行时对齐检查(特别是如果它需要原子 RMW 来读取),而不是仅仅决定它永远不会 lock_free,如果 new 对齐小于该大小。任何主流 x86 实现都不是这种情况,例如我认为 MSVC 在 x86-64 上对齐 16(GNU/Linux 肯定如此),并且在 32 位模式下所有内容至少对齐 8。 IDK alignof(max_align_t) 在 AArch64 / MIPS64 / PPC64 的 gcc 上是什么。我认为 AArch64 将有 16 字节的原子基线,甚至不需要 -march 选项,但很可能。 16B新 @PeterCordes,我们知道在哪里可以查询许多配置godbolt.org/z/73z11c49e

以上是关于为啥 std::atomic<T>::is_lock_free() 不像 constexpr 一样是静态的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥即使删除了复制构造函数,std::atomic 也会从 C++17 编译?

在 C++11 中以无锁方式原子交换两个 std::atomic<T*> 对象?

如何在 std::atomic<T> 上实现一个简单的自旋锁,以便编译器不会对其进行优化?

C++ 原子操作 std::atomic<int>

std::atomic<std::string> 是不是正常工作?

队列<T>上的原子操作?