有没有办法绕过原子操作的成本?
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
我有一个计数器,目前是一个原子 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