为啥标准库不以无锁方式为 8 字节以下的结构实现 std::atomic?

Posted

技术标签:

【中文标题】为啥标准库不以无锁方式为 8 字节以下的结构实现 std::atomic?【英文标题】:Why don't standard libraries implement std::atomic for structs under 8 bytes in a lock-free manner?为什么标准库不以无锁方式为 8 字节以下的结构实现 std::atomic? 【发布时间】:2019-09-17 13:56:57 【问题描述】:

假设体系结构可以为 std::atomic 以无锁方式支持 8 字节标量。为什么标准库不为 8 字节以下的结构提供类似的特化?

这种 std::atomic 特化的简单实现可以将结构体序列化/反序列化(使用std::memcpy)成等效的std::uintx_t,其中x 是结构体的宽度(以位为单位)(四舍五入到大于或等于结构宽度的 2 的最接近幂)。这将是很好的定义,因为 std::atomic 要求这些结构可以轻松复制。

例如。 https://godbolt.org/z/sxSeId,这里Something只有3个字节,但是实现调用__atomic_load__atomic_exchange,都使用了锁表。

【问题讨论】:

gcc 如果您将结构设为 4 个字节(但不是 3 个字节),则它会正确,请参阅godbolt.org/z/d1OCmG。铿锵声没有。 @PaulSanders 有趣,我想知道为什么 3 个字节不起作用.. 没有 x86 指令可以加载/存储 3 个字节,更不用说原子了。 @rustyx 啊,对不起,但你总是可以占用超过 2 的下一个幂的大小,对吧? §[atomics.types.generic]p3 部分允许这样做 - 原子特化的表示不需要与其相应的参数类型具有相同的大小。我猜这有可移植性问题? @Curious:当我说“强制对齐”时,我指的是alignas(4) 【参考方案1】:

Linux 的 atomic<T> 不幸的是(?)不对齐 / 填充到 2 的幂大小。 std::atomic<Something> arr[10] 的 sizeof(arr) = 30。(https://godbolt.org/z/WzK66xebr)


使用struct Something alignas(4) char a; char b,c; ; (不是alignas(4) char a,b,c;,因为这会使 each 字符填充到 4 个字节,以便它们每个都可以对齐。)

具有非 2 次幂大小的对象可能跨越缓存线边界,因此并非总是可以使用更宽的 4 字节负载。

另外,纯存储总是必须使用 CAS(例如lock cmpxchg)来避免发明写入对象外部的字节:显然你不能使用两个单独的mov 存储(2 字节 + 1 字节) 因为这不是原子的,除非您在 TSX 事务中使用重试循环执行此操作。


x86 加载/存储仅保证不跨越 8 字节边界的内存访问是原子的。 (在某些供应商/uarches 上,缓存线边界。或者对于可能无法缓存的加载/存储,基本上自然对齐是您所需要的)。 Why is integer assignment on a naturally aligned variable atomic on x86?

您的struct Something char a, b, c; ; 没有对齐要求,因此没有阻止Something 对象跨越2 个缓存行的C++ 规则。这将使它的普通-mov 加载/存储绝对不是原子的。

gcc 和 clang 选择使用与 T 相同的布局/对象表示来实现 atomic<T>(无论是否无锁)。因此atomic<Something> 是一个 3 字节的对象。因此,atomic<Something> 的数组必然有一些跨越高速缓存行边界的对象,并且不能在对象外部进行填充,因为这不是数组在 C 中的工作方式。sizeof() = 3 告诉您数组布局。 这使得无锁 atomic<Something> 成为不可能。(除非您使用 lock cmpxchg 加载/存储即使在缓存行拆分时也是原子的,否则在这样做的情况下会产生巨大的性能损失发生。最好让开发人员修复他们的结构。)

atomic<T> 类可以比T 有更高的对齐要求,例如atomic<int64_t> 有 alignof(atomic_int64_t) == 8,这与许多 32 位平台上的alignof(int64_t) == 4 不同(包括 i386 System V ABI )。

如果 gcc/clang 没有选择保持布局相同,他们可以让 atomic<T> 填充小对象到 2 的下一个幂并添加对齐,以便它们可以无锁。这将是一个有效的实施选择。我想不出任何缺点。


有趣的是,gcc 的 C11 _Atomic 支持是 slightly broken on 32-bit platforms with 64-bit lockless atomics :_Atomic int64_t 可能在结构内部错位导致撕裂。他们仍然没有更新 _Atomic 类型的 ABI 以实现自然对齐。

但是 g++ 的 C++11 std::atomic 在头文件中使用了一个模板类,不久前修复了该错误 (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147);确保 atomic<T> 具有自然对齐(最大为 2 大小的幂),即使 T 具有对齐

【讨论】:

哦,缓存线拆分确实很有意义,谢谢!

以上是关于为啥标准库不以无锁方式为 8 字节以下的结构实现 std::atomic?的主要内容,如果未能解决你的问题,请参考以下文章

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

动态无锁内存分配器

多个线程可以对数组中的不同字节进行“原子”无锁写入吗?

为啥 Typescript 不以正确的方式支持函数重载?

为啥不以编程方式快速更改 uibutton 标题?

为啥 CheckBox 检查不以编程方式与 Kotlin 一起工作?