相比于有或没有争用的锁,原子/交错变量有多快?

9

相对于无争议的原子变量(例如 C++ 的 std::atomic<T> 操作),它的速度快/慢多少。

此外,有争议的原子变量相对于无争议的锁会慢多少?

我正在使用的架构是 x86-64。


@KonradRudolph,我看到这些问题有相似之处,但并不完全相同。这一个更加关注操作的基本成本,而另一个则是两种算法方法的开销成本。实际上,我会对它们做出稍微不同的回答。 - edA-qa mort-ora-y
作为另一个问题的作者,我可以说它们是相同的。另一个问题可能在表述上有所不同(在开销方面),但它实际上询问的是:“原子操作比锁快多少?” - Konrad Rudolph
3个回答

15

我碰巧有很多低级速度测试结果。然而,速度的确切含义是非常不确定的,因为它在很大程度上取决于您正在做什么(即使与操作本身无关)。

以下是一些来自 AMD 64 位 Phenom II X6 3.2Ghz 的数字。我也在 Intel 芯片上运行过此测试,时间差异很大(同样取决于具体要执行的操作)。

使用 GCC 的 __sync_fetch_and_add,这将是一个完全同步的原子加法,平均需要 16ns,最短时间为 4ns。最短时间可能更接近真实值(但那里也有一些开销)。

一个未被争用的 pthread 互斥量(通过 boost 实现)需要 14ns(这也是其最短时间)。请注意,这也有点太短了,因为如果其他东西锁定了互斥量但现在没有争用(因为它会导致缓存同步),时间实际上会增加。

一个失败的 try_lock 需要 9ns。

我没有普通的原子递增,因为在 x86_64 上,这只是一个普通的交换操作。可能接近最小可能时间,所以是 1-2ns。

在条件变量上没有等待者的情况下调用 notify 需要 25ns(如果有大约 304ns 的等待线程)。

然而,由于所有锁定都会导致某些 CPU 引导保证,所以您修改的内存量(适合存储缓冲区的任何内容)将改变这些操作所需的时间。显然,如果互斥量存在争用,那么这将是最差的情况。即使没有实际发生线程切换,任何返回到 Linux 内核的操作也可能需要数百纳秒。这通常是原子锁表现优异的地方,因为它们不涉及任何内核调用:您的平均情况性能也是最差情况的性能。如果有等待线程,则互斥解锁也会产生开销,而原子解锁则不会。


注意:进行此类测量存在很多问题,因此结果总是受到质疑。我的测试尝试通过固定 CPU 速度、为线程设置 cpu 亲和性、不运行其他进程并对大型结果集进行平均来尽可能减少变化。


谢谢提供这些数字!你是在哪个平台上测试的?仅说“pthread mutex”并不能说明太多,因为其含义完全取决于实现方式。由于时间接近原子加操作,我猜测你是在GNU/Linux上使用futex进行测试? - Jonathan Wakely
是的,在Linux上。不争用意味着它不会触及系统调用,因此在这种情况下futex实际上并没有参与其中(NPTL库中的非争用完全在用户空间中解决,没有系统调用)。 - edA-qa mort-ora-y
在我的看法中,“futex” 就是整数,因此它是涉及到的,但是只需要对“futex”(即整数)进行原子递增即可。 - Jonathan Wakely
1
使用xchg不可能进行原子递增操作(即使它有隐式的lock前缀)。在大多数CPU上,lock add [mem], 1的成本几乎与lock xadd [mem], eax一样高,只是稍微简单了一点。它肯定不会像1ns(3GHz CPU上的3个时钟周期)那么快,lock前缀的完整屏障不能阻止非内存指令的乱序执行。Agner Fog的指令表中没有K10的lock数字,但Piledriver lock add每约40个周期执行一次(与xchg [mem],reg相同),而lock xadd每约39个周期执行一次。 - Peter Cordes

6

有一个GitHub上的项目,旨在测量不同平台上的这种情况。不幸的是,在我的硕士论文之后,我从未真正有时间跟进,但至少基本代码已经存在。

它测量pthread和OpenMP锁,与__sync_fetch_and_add内部函数进行比较。

据我记得,我们预计锁和原子操作之间会有很大的差异(约为一个数量级),但实际差异非常小。

然而,现在在我的系统上进行测量得出的结果反映了我的最初猜测,即无论使用pthread还是OpenMP,原子操作都快约五倍,单个锁定增量操作需要约35纳秒(这包括获取锁,执行增量以及释放锁)。


我认为高争用(contention)和低争用可以影响很多。如果缓存行(锁和数据或者原子操作只有数据)在当前核心处于 MESI Modified 或 Exclusive 状态,那么获取和释放锁或者 x86 的 lock add [mem], 1 都非常快。但是,无论如何,这很难进行微基准测试,因为在某些 ISA 上,弱排序的原子增量(例如 std::memory_order_relaxed)避免了内存屏障,其成本取决于正在等待的其他负载/存储的数量和无法重新排序的数量。 - Peter Cordes
我不知道你在Github上的代码是否有很多线程什么都不做,只是试图增加同一个变量,但这通常不太现实。如果你有一个真正的程序大部分时间都在这样做,将其改为单线程将是一个胜利。无锁RMW原子操作通常比无争用情况下的锁定/解锁更快(没有函数调用开销,少了一些汇编指令),但在只读情况下读者永远不必获取锁时,可以快得多 - Peter Cordes

4

这取决于锁的实现方式,也取决于系统。原子变量不能像锁一样真正地争用(即使您使用获取-释放语义),这就是原子性的全部意义,它锁定总线以传播存储(取决于内存屏障模式),但这是一个实现细节。

然而,大多数用户模式锁只是封装的原子操作,可以查看Intel的this文章,了解在x86和x64下使用原子操作的高性能、可扩展锁的一些数据(与Windows的CriticalSection锁进行比较,不幸的是,没有关于SWR锁的统计数据,但应该始终为自己的系统/环境进行分析)。


4
“原子变量不能像锁一样被真正地争用” - 如果两个线程(在不同的内核上)同时操作同一个原子变量,那么这就是争用,对于是否会导致减慢速度,取决于架构/实现。你可以将其与两个在不同内核上操作相同的非原子变量进行比较,以了解原子同步在某种意义上是否需要时间。 - Steve Jessop
1
@SteveJessop,没错。两个核心使用相同的变量会导致该变量过度同步。此时,您受到缓存总线的延迟/带宽的限制。 - edA-qa mort-ora-y
@SteveJessop:你可以这么称呼它,但在我看来,它的实现方式完全不同,因此你不能把它和已经获取锁的自旋重试放在同一类别中。 - Necrolis
1
@edA-qamort-ora-y:问题可能在类x86的架构上会变得混乱,因为存在一致性缓存。就像你所说的那样,即使它不是原子变量,重复访问同一位置也是一种争用。我不确定提问者是否知道这一点,但我认为如果你想找出有争议的原子增量的“成本”,这是一个混淆因素。你可以将其与单线程中的原子增量或有争议的非原子增量(又称数据竞争)进行比较,并得出非常不同的“原子争用”成本概念。 - Steve Jessop
@Necrolis:当然,机制完全不同,但我认为提问者称所有这些事情为“争用”是正确的。如果我的代码因等待其他代码让路而延迟,那么无论机制如何,我们都在竞争。 :-) - Steve Jessop
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接