有没有办法绕过原子操作的成本?

Are there ways of getting around the cost of atomic operations?

我有一个计数器,目前是一个原子 u32,在我的代码的热门部分使用,它通常只增加 1。偶尔,它会从代码的一个非常不同的部分读取,但如果那确实发生,该值必须准确(至少在同一线程上)。但是,我怀疑原子性可能会对性能产生不良影响。我必须解决的一个想法是让主计数器是非原子的,但以原子方式写入第二个计数器。

原子写入比读取便宜吗?就像它不需要清除(那么多)缓存吗?

TL;DR

如果您只有一个编写器和一个 reader 线程,您只需使用具有 relaxed memory ordering or acquire/release.

的原子

详情

在 x86 上,它将被转换为正常的 add/mov 指令,因此不会对性能产生影响。

这是一个正常的计数器增量:

example::normal_inc:
        add     dword ptr [rip + example::normal_u32], 1
        ret

这是一个松散排序的原子计数器增量:

example::atomic_inc:
        add     dword ptr [rip + example::atomic_u32], 1
        ret

在 x86 上没有区别,因此没有性能影响。但是代码正确吗?

放宽loads/stores不保证跨线程的顺序,只保证同线程的顺序和原子性。这是什么意思?

对于一个作者和一个 reader 案例,这意味着如果线程 W 更新计数器,线程 R 最终将看到更改,并且该值将有效,因为保证了原子性。例如,如果计数器为 0,线程 W 将其增加到 1 和 2,则保证线程 R 最终会看到 2,而它永远不会看到 42 或其他随机数。

不能保证的是这个数字将与其他原子或非原子变量对齐。比如说,如果线程 W 将元素添加到列表然后增加计数器,线程 R 可能会以相反的顺序看到这些事件,即首先计数器增加,然后新元素出现在列表。

仍然可以保证的是,从线程的角度来看,事件的顺序 W。使用列表的相同示例,可以保证对于线程 W,列表元素将在计数器增加之前出现,因为所有这些更改都发生在同一线程内,而不是跨不同的线程。

由于 x86 具有很强的内存排序,即使 aquire/release 原子排序仍然使用正常的 add/mov 操作。参见 memory ordering on Wikipedia

Acquire/release 语义不仅保证了原子性,还保证了顺序。以列表为例,线程 W 添加一个列表元素,然后 releases 添加一个计数器。当线程 R acquires 计数器时,保证列表元素在那里。在 x86 上,此保证无需额外费用。

另请参阅上面关于 Godbolt 的示例:https://godbolt.org/z/4EsY4j