当外部基础类型未按要求对齐时的atomic_ref

Ale*_*iev 2 c++ memory-alignment undefined-behavior stdatomic c++20

我在p0019r8上读到以下内容:

atomic_ref(T& obj);
Run Code Online (Sandbox Code Playgroud)

要求:引用的对象应与 对齐required_alignment

当未对齐时, cppreference将其解释为 UB:

如果 obj 未与 required_alignment 对齐,则行为未定义。


那么您期望实现如何处理它呢?

并且实现可以检查编译时 alignof,但实际上类型可能比对齐更对齐alignof。实现可以解释指针位并检查运行时对齐,但这是额外的运行时检查。

最终我看到以下选项:

  • 什么都不做 - 以令人不快的方式实现运行时未定义的行为,仅支持正确的使用
  • 检查编译时对齐 ( alignof) 并在错误时发出警告
  • 检查编译时对齐 ( alignof) ,如果错误不正确,则编译时失败,因为实际对齐可能大于静态类型可见的对齐
  • 检查编译时对齐 ( alignof),如果错误不正确,则在运行时失败,因为实际对齐可能大于静态类型可见的对齐
  • 检查编译时对齐 ( alignof) 并在错误时回退到基于锁
  • 检查运行时对齐(指针位),如果错误则在运行时失败
  • 检查运行时对齐(指针位),如果错误则回退到基于锁

Pet*_*des 6

TL:DR:永远不要默默地回退到锁定,没有人希望这样做,因为它违背了std::atomic. 将非无锁视为可移植性的后备方案,而不是可行的操作模式。


UB 使得编译器可以合法地简单地假设而不检查它是否对齐。能够在没有任何运行时检查的情况下进行假设是UB 概念的主要好处之一。这是大多数人在运行时在优化构建中想要/期望的,而不是使用可能回退到使用互斥体的条件分支来使代码膨胀。

选择是否(以及如何)在这里定义任何行为完全取决于实现,这是实现质量以及性能与调试之间权衡的问题。我认为您知道这一点,并且实际上是在询问用户希望编译器为这些 QoI 选择选择什么,这很好。

正如您链接的 P0019 提案所说,这一切都归结为 QOI 问题:

  1. 参考能力约束

原子引用引用的对象必须满足可能的特定于体系结构的约束。例如,对象可能需要在内存中正确对齐,或者可能不允许驻留在 GPU 寄存器内存中。我们不会枚举所有潜在的约束或指定违反这些约束时的行为。当违反约束时生成适当的信息是一个实施质量问题。

“生成适当的信息”措辞意味着他们希望实现在检测到违规时发出警告/错误,而不是回退到锁定。

尽管可以回退到锁定的实现可能会愚蠢地设置required_alignment为正确性的最小值 (1),而不是无锁的最小值。当然没有人希望这样,但这是一个 QoI 问题,而不是标准合规性问题。


我期望(或者至少希望)实现如下:

  • 如果在任何小于 的atomic_ref对象上使用,则在编译时发出警告。您可能知道某个字节恰好是 8 字节对齐的,即使只有 1 或 4 个字节,因此这不应该是一个错误。alignofrequired_alignmentT *palignof(T)

    一些当地的方法来消除警告将是一件好事。(替代方案:使用 GNU C 之类的东西来保证与编译器的对齐x = __builtin_assume_aligned(x, 16)

    至少在编译时明确知道某个对象未对齐时发出警告,例如,其对齐方式已知的结构的子成员,或者声明可见但不包含的全局变量alignas通过可能未对齐的指针进行访问的警告会产生更多噪音,应单独禁用。

  • 超慢调试模式:运行时检查对齐情况,警告或中止原子性未对齐的特定对象。(例如gcc -fsanitize=undefined,或者 MSVC 的调试模式已经添加了诸如std::vector::operator[]边界检查之类的内容。我认为 GCC 的 UBSan 比 MSVC 调试模式做了更多的检查,例如签名溢出;我认为 MSVC 调试模式介于 和 之间gcc -O0gcc -O0 -fsanitize=undefined

  • “Release”模式:零检查,只发出asm,其正确性取决于正在对齐的对象。(还有-O0不带 UBSan 的 gcc,它允许一致的调试,但不添加额外的检查。)

  • 没有人希望在编译时或运行时静默回退到互斥体。这种操作模式基本上就存在,因此 ISO C++ 可以要求该功能在所有地方都得到支持,而不会使其无法在某些目标上实现。

    与关键部分的手动细粒度锁定(在为其设计的数据结构上同时执行一些相关的原子操作)相比,回退到锁定通常是非常次优的。人们使用atomic<T>(以及即将到来的atomic_ref<T>)来提高性能,而大部分性能都被锁定破坏了。尤其是读取端的可扩展性。

脚注 1:IIRCalignof()仅针对类型而不是对象指定,但在 GNU C++ 中它也适用于对象。我使用它作为编译器内部知识的简写,即某个对象用于alignas()过度对齐它。

  • @AlexGuteniev:我认为您通常会在每次使用它时构造一个新的“atomic_ref”,或者每个函数调用至少构造一次并在循环中重用它。除了本地临时存储之外,您不想将其存储在任何地方;这会为生成的 asm 添加无用的间接级别。(构造本地 tmp `atomic_ref` 应该花费 0 个 asm 指令)。因此,我希望 `atomic_ref` 构造函数运行得非常频繁,这是通过 `atomic_ref` 完成的原子操作数量的重要部分。 (3认同)