在C++中,用于原子更新的 CAS 最有效的实施是什么?

What's the most efficient implementation of CAS for atomic updates in C++?

本文关键字:CAS 有效 是什么 更新 C++ 用于      更新时间:2023-10-16

我编写并行程序,该程序使用 CAS 操作来更新线程之间的共享内存。(C++, Linux, x86)

以下是我对"Update"函数的实现,该函数对变量 a 指向的内存位置应用更新,该位置由 f(*a, b) 返回的值。

inline bool CAS (int64_t* ptr, int64_t old_val, int64_t new_val) {
return __sync_bool_compare_and_swap(ptr, old_val, new_val); 
}
inline void Update(int64_t* a, int64_t& b) {
volatile int64_t expected, result;
do {
expected = *a;
result = f(expected, b);
} while (!CAS(a, expected, result));
}

我看到大多数其他实现使用几乎相同的代码。

但我只是想知道它是否是最有效的,因为我从Vtune剖面器中看到相当高的CPI率(1.2~1.5)。

如果从嵌套计算循环的最内部循环调用更新函数,则执行...而带有分支的 ()循环会导致严重的分支错误预测。但考虑到 CAS 的语义包括用于比较的分支,这可能是不可避免的。

在任何情况下,上述更新功能是否有任何首选实现? 例如,在某些情况下,比较交换强可以击败比较交换弱。如果更新函数中的函数 f 用于加法,则最好使用 std::atomic 提供的atomic_fetch_and_add。

这是带有注释的更新代码(没有观察到性能提升,我正在微优化。但无论如何,在最坏的情况下可能会更好)

inline bool CAS (int64_t* ptr, int64_t& old_val, int64_t new_val) {
return (std::atomic_compare_exchange_weak((std::atomic<int64_t>*) ptr, &old_val, new_val); 
}
inline void Update(int64_t* a, int64_t& b) {
int64_t expected, result;
do {
expected = *a;
result = f(expected, b);
} while (!CAS(a, expected, result));
}

标准库在<atomic>中有一个可移植的实现,atomic_compare_exchange_weak()函数族。 您可能会从中获得更好的性能。 如果读取器线程只需要一些快照,则可以使用宽松的内存顺序执行原子读取,或者在需要最新快照时获取。 宽松的内存顺序可能与读取内存一样简单。

但是,大多数性能改进可能来自更好的免等待数据结构和算法。 对于 CAS 来说,单向链表通常是一种非常快速的免等待结构。

有一些特殊情况。我相信您知道,如果只有一个线程是编写器,则其他线程可以简单地使用获取/发布语义甚至宽松的内存顺序读取更新。(或者,作为 gcc/clang 扩展以匹配您使用的内置,通过volatile*

如果您经常看到其他线程完成并尝试同时更新,则可能有一种方法可以更改算法以将工作线程隔开。 在某些算法中,看到更新的线程可能会退缩并让位于其他线程。

还要警惕 A-B-A 错误。 您似乎没有检查它。 如果不需要,则可以利用cmpxch16b指令一次对 16 字节结构进行 CAS 处理,并获得比在单个指针上使用 CAS 更好的原子更新。