与锁相比,原子/互锁变量有多快,无论是不是存在争用? [复制]
Posted
技术标签:
【中文标题】与锁相比,原子/互锁变量有多快,无论是不是存在争用? [复制]【英文标题】:How fast is an atomic/interlocked variable compared to a lock, with or without contention? [duplicate]与锁相比,原子/互锁变量有多快,无论是否存在争用? [复制] 【发布时间】:2012-06-16 05:42:48 【问题描述】:与无争议的原子变量(例如 C++ 的 std::atomic<T>
)操作相比,它的速度快/慢多少。
另外,有争议的原子变量相对于无争议的锁要慢多少?
我正在研究的架构是 x86-64。
【问题讨论】:
@KonradRudolph,我看到问题相似但不完全相同。这个更关注运营的基本成本,而另一个是算法的两种方法的间接成本。我实际上会以不同的方式回答它们。 @edA-qamort-ora-y 作为另一个问题的作者,我可以说它们是相同的。另一个问题可能措辞不同(就开销而言),但它实际上要问的是“原子操作比锁快多少?” 【参考方案1】:我碰巧有很多低级速度测试。然而,速度究竟意味着什么是非常不确定的,因为它在很大程度上取决于你到底在做什么(甚至与操作本身无关)。
以下是 AMD 64 位 Phenom II X6 3.2Ghz 的一些数字。我也在英特尔芯片上运行过它,而且时间确实有很大差异(同样,具体取决于正在做的事情)。
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? 是的,在 Linux 上。 Uncontested 意味着它不涉及系统调用,因此 futex 实际上并没有参与这种情况(NPTL 库中的非竞争完全在用户空间中解决,没有系统调用)。 在我看来“futex”是整数,所以它涉及到,但所需要的只是“futex”(即整数)的原子增量 原子增量不适用于xchg
(即使它有一个隐含的lock
前缀)。 lock add [mem], 1
在大多数 CPU 上几乎与 lock xadd [mem], eax
一样昂贵,只是稍微简单一些。它肯定不会像 1ns(3GHz CPU 上的 3 个时钟)那么快,lock
前缀的完整障碍不会阻止非内存指令的乱序执行。 Agner Fog 的指令表没有来自 K10 的 lock
数字,但 Piledriver lock add
是每 ~40 个周期一个(与 xchg [mem],reg
相同),而 lock xadd
是每 ~39 个周期一个。【参考方案2】:
有一个project on GitHub,目的是在不同平台上衡量这一点。不幸的是,在我的硕士论文之后,我从来没有真正有时间跟进这个,但至少基本代码在那里。
与 __sync_fetch_and_add
内在函数相比,它测量 pthread 和 OpenMP 锁。
据我记忆,我们预计锁和原子操作之间会有相当大的差异(〜一个数量级),但实际差异却非常小。
但是,现在在我的系统上测量得到的结果反映了我最初的猜测,即(无论使用 pthread 还是 OpenMP)原子操作的速度大约快五倍,单个锁定增量操作大约需要 35ns(这包括获取锁,执行增量,释放锁)。
【讨论】:
我认为您有高竞争还是低竞争可能很重要。如果缓存行(锁和数据,或只是原子数据)在当前内核上仍处于 MESI 修改或独占状态,则获取和释放锁或 x86lock add [mem], 1
都非常快。但无论如何,很难进行微基准测试,因为在某些 ISA 上,弱排序的原子增量(如 std::memory_order_relaxed)避免了内存屏障,其成本取决于可能有多少 other 加载/存储正在飞行中,无法重新订购。
IDK 如果你在 github 上的代码有很多线程什么都不做,只是敲击同一个变量试图增加它,但这通常不是很现实。如果你有一个真正的程序花费大部分时间来做这件事,那么让它成为单线程将是一个胜利。无论如何,在无竞争情况下,无锁 RMW 原子通常比锁定/解锁要快一些(没有函数调用开销,并且更少的 asm 指令),但在读取时可以快得多 - 读者永远不必获取锁的唯一情况。【参考方案3】:
取决于锁的实现,也取决于系统。原子变量不能以与锁相同的方式真正受到竞争(即使您使用的是acquire-release semantics),这就是原子性的全部意义,它锁定总线以传播存储(取决于内存屏障模式),但这是一个实现细节。
但是,大多数用户模式锁只是封装了原子操作,请参阅 Intel 的 this 文章,了解有关在 x86 和 x64 下使用原子操作的高性能、可扩展锁的一些数据(不幸的是,与 Windows 的 CriticalSection
锁相比) ,找不到SWR 锁的统计信息,但应始终针对自己的系统/环境进行分析。
【讨论】:
“原子变量不能像锁一样被真正地争夺”——如果两个线程(在不同的内核上)敲击同一个原子变量,那么这就是争夺它,确定吗?然后由架构/实现决定是否竞争实际上会减慢速度。您也许可以将其与不同内核上的两个线程敲击相同的非原子变量进行比较,以了解原子同步在某种意义上是否需要任何时间。 @SteveJessop,当然。使用相同变量的两个内核将导致该变量的过度同步。此时,您受制于缓存总线的延迟/带宽。 @SteveJessop:你可以这样称呼它,但是,IMO,它以不同的方式一起完成,因此你不能真正将它与旋转等待重试放在同一类别中已经获得锁。 @edA-qamort-ora-y: 这个问题在类似 x86 的架构上可能会因为连贯缓存而混淆。所以就像你说的那样,敲打同一个位置是一种争用,即使它不是一个原子变量。我不确定提问者是否知道这一点,但我认为如果您着手找出有争议的原子增量的“成本”是多少,这是一个令人困惑的因素。您可以将其与单个线程中的原子增量或有争议的非原子增量(也称为数据竞赛)进行比较,然后就“原子争用”的成本提出非常不同的想法。 @Necrolis:当然,机制完全不同,但我认为提问者将所有这些事情称为“争用”是正确的。如果我的代码延迟等待其他代码退出,那么无论机制如何,我们都在竞争 :-)以上是关于与锁相比,原子/互锁变量有多快,无论是不是存在争用? [复制]的主要内容,如果未能解决你的问题,请参考以下文章