我有一个计数器,目前是一个原子 u32,在我的代码的热门部分中使用,它通常只增加 1。偶尔,它会从代码的不同部分读取,但如果确实发生这种情况,该值必须准确(至少在同一线程上)。但是,我怀疑原子性可能会对性能产生不良影响。我必须解决这个问题的一个想法是让主计数器是非原子的,但以原子方式写入第二个计数器。
原子写入比读取便宜吗?就像它不需要清除(尽可能多的)缓存吗?
And*_*kyy 11
如果您只有一个写入器和一个读取器线程,那么您所需要做的就是使用具有宽松内存排序或 acquire/release 的原子。
在 x86 上,它将被转换为正常的 add/mov 指令,因此不会对性能产生影响。
这是正常的计数器增量:
example::normal_inc:
add dword ptr [rip + example::normal_u32], 1
ret
Run Code Online (Sandbox Code Playgroud)
这是一个具有宽松排序的原子计数器增量:
example::atomic_inc:
add dword ptr [rip + example::atomic_u32], 1
ret
Run Code Online (Sandbox Code Playgroud)
x86 上没有差异,因此不会影响性能。但代码正确吗?
宽松的加载/存储不保证跨线程的顺序,但保证同一线程上的顺序和原子性。这是什么意思?
对于一名写入者和一名读取者的情况,这意味着如果线程W更新计数器,线程R最终将看到更改,并且该值将有效,因为保证了原子性。例如,如果计数器为 0,并且线程W将其增加到 1 和 2,则保证线程R最终会看到 2,并且永远不会看到 42 或其他随机数。
无法保证该数字将与其他原子或非原子变量对齐。比如说,如果线程W将元素添加到列表中,然后增加计数器,则线程R可能会以相反的顺序看到这些事件,即首先增加计数器,然后一个新元素出现在列表中。
仍然可以保证的是从线程的角度来看事件的顺序W。对于列表的相同示例,可以保证对于线程,W列表元素将在计数器增加之前出现,因为所有这些更改都发生在同一线程内,而不是跨不同线程。
由于 x86 具有相当强大的内存排序,即使原子上的获取/释放排序仍然使用正常的添加/移动操作。请参阅Wikipedia 上的内存排序。
获取/释放语义不仅保证原子性,而且保证顺序。以列表为例,线程W添加一个列表元素,然后添加releases一个计数器。当线程化R acquires计数器时,可以保证列表元素在那里。在 x86 上,此保证无需额外费用。
另请参阅上面有关 Godbolt 的示例:https://godbolt.org/z/4EsY4j