CPU原生不支持的大尺寸std::atomic是如何实现的?
问题描述
当前C++编译器支持CPU原生不支持的大尺寸原子操作,例如x64平台原生支持16字节原子操作,但
std::atomic可处理更大的结构体。以下为示例代码:#include <iostream> #include <atomic> using namespace std; struct S { size_t a, b, c; }; atomic<S> apss; int main() { auto ref = apss.load( memory_order_relaxed ); apss.compare_exchange_weak( ref, { 123, 456, 789 } ); cout << sizeof ::apss << endl; }在我的平台上,
cout始终输出32。但这类原子操作无需mutex是如何工作的?通过反汇编无法找到线索。若使用MSVC++运行以下代码:
#include <atomic> #include <thread> #include <array> using namespace std; struct S { size_t a, b, c, d, e; }; atomic<S> apss; int main() { array<jthread, 2> threads; auto threadFn = []() { auto ref = apss.load( memory_order_relaxed ); for( size_t i = 10'000'000; i--; apss.compare_exchange_weak( ref, { } ) ); }; threads[0] = jthread( threadFn ); threads[1] = jthread( threadFn ); }该代码几乎不消耗内核时间,竞争完全发生在用户态,推测这可能基于软件事务内存实现。
解答
核心实现机制
当原子变量的类型大小超出CPU原生支持的原子操作宽度(如x64平台的16字节上限)时,编译器会采用用户态无锁方案替代内核态mutex,主要有两种实现路径:
软件事务内存(STM)
你观察到MSVC下低内核消耗的现象确实对应STM实现。STM在用户态跟踪线程对内存的访问操作,当检测到多个线程竞争同一原子变量时,会直接在用户态执行重试或回滚逻辑,完全不需要调用内核的mutex接口,因此几乎不会产生内核态耗时。乐观锁+CAS循环的复合结构
部分编译器会给大尺寸原子变量附加一个隐藏的"版本标记"或"锁状态"字段——这就是你看到sizeof(apss)输出32的原因:结构体S本身占24字节,加上额外8字节的标记字段后对齐到32字节。操作流程如下:- 读取时同时获取数据内容和对应的版本标记
- 修改数据后,检查版本标记是否未被其他线程修改,若未变化则通过CAS原子更新数据和标记的组合
- 若标记已变化,说明存在竞争,直接重试整个操作
反汇编无线索的原因
编译器会将大尺寸std::atomic的操作封装为标准库的内部函数调用(比如MSVC下的_Atomic_load、_Atomic_compare_exchange_weak系列函数)。仅反汇编主函数只能看到这些库函数的调用指令,无法直接看到内部的无锁或STM逻辑。需要反汇编对应的标准库二进制文件(如vcruntime140.dll)才能查看具体实现细节。
内容的提问来源于stack exchange,提问作者Edison von Myosotis

