为啥 OSX 上的 std::mutex 这么慢?

Posted

技术标签:

【中文标题】为啥 OSX 上的 std::mutex 这么慢?【英文标题】:Why is std::mutex so slow on OSX?为什么 OSX 上的 std::mutex 这么慢? 【发布时间】:2014-04-06 19:43:55 【问题描述】:

我有以下基准:https://gist.github.com/leifwalsh/10010580

基本上它会启动k 线程,然后每个线程使用自旋锁和std::mutex 执行大约1600 万/k 锁定/增量/解锁周期。在 OSX 上,std::mutex 在竞争时比自旋锁慢得多,而在 Linux 上它具有竞争力或更快。

OSX:

spinlock 1:     334ms
spinlock 2:     3537ms
spinlock 3:     4815ms
spinlock 4:     5653ms
std::mutex 1:   813ms
std::mutex 2:   38464ms
std::mutex 3:   44254ms
std::mutex 4:   47418ms

Linux:

spinlock 1:     305ms
spinlock 2:     1590ms
spinlock 3:     1820ms
spinlock 4:     2300ms
std::mutex 1:   377ms
std::mutex 2:   1124ms
std::mutex 3:   1739ms
std::mutex 4:   2668ms

处理器不同,但没有那么不同(OSX 是 Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz,Linux 是 Intel(R) Core(TM) i5- 2500K CPU @ 3.30GHz),这似乎是库或内核问题。有人知道缓慢的根源吗?

为了澄清我的问题,我理解“有不同的互斥锁实现可以针对不同的事物进行优化,这不是问题,这是意料之中的”。这个问题是:导致这种情况的实际实施差异是什么?或者,如果是硬件问题(也许 macbook 上的缓存慢了很多),这也是可以接受的。

【问题讨论】:

移动 CPU 有 2 个内核和超线程(运行速度只有一半,但这可能不是问题)。台式机 CPU 有 4 个真正的内核。这似乎是一个非常很大的差异。 您链接到哪个标准 C++ 库? libstdc++ 还是 libc++? osx 上的 libc++,linux 上的 libstdc++ 最大的不同可能是在 MacOS X 上没有人关心。您的代码是用 Objective-C 编写的,对象属性的原子设置器很快,@synchronized 也很快。一直存在原子设置/添加/或操作。并且有可能代码没有针对病理情况进行优化,而是针对通常情况进行优化。 这几乎可以肯定主要是由于一个锁是公平的(即按 FIFO 顺序唤醒等待线程)而另一个不是。不过,在更新的操作系统版本上,无竞争的采集时间应该会好很多。 (虽然如果 Linux std::mutex 实际上是一个公平的锁,那是非常令人印象深刻的) 【参考方案1】:

您只是在衡量图书馆选择以吞吐量换取公平性的选择。该基准非常人为,并且会惩罚任何提供任何公平性的尝试。

实现可以做两件事。它可以让同一个线程连续两次获得互斥锁,也可以改变哪个线程获得互斥锁。由于上下文切换需要时间,而且在缓存之间对互斥体和 val 进行 ping-ponding 需要时间,因此该基准会严重惩罚线程的更改。

很可能,这只是展示了实现必须做出的不同权衡。它极大地奖励了喜欢将互斥锁返回给最后持有它的线程的实现。基准测试甚至奖励那些浪费 CPU 的实现!它甚至奖励那些浪费 CPU 以避免上下文切换的实现,即使 CPU 可以做其他有用的工作!它也不会惩罚可能减慢其他不相关线程的内核间流量的实现。

此外,实现互斥锁的人通常认为在非竞争情况下的性能比在竞争情况下的性能更重要。您可以在这些情况之间做出许多权衡,例如假设可能有线程等待或专门检查是否存在。基准测试仅(或至少,几乎仅)测试通常会权衡的情况,以支持假定更常见的情况。

坦率地说,这是一个无法识别问题的毫无意义的基准。

具体的解释几乎可以肯定,Linux 实现是 spinlock/futex 混合体,而 OSX 实现是常规的,相当于锁定一个内核对象。 Linux 实现的自旋锁部分倾向于允许刚刚释放互斥锁的同一个线程再次锁定它,这对您的基准测试有很大的回报。

【讨论】:

那我不明白你不明白的。你如何做出这些权衡决定了你在这个基准上的表现。不同的实现使这些权衡不同。你说这似乎是个问题,我在解释为​​什么它不是问题(基准测试除外)。 我在问有什么区别,你的回答是“有一些”。我没有说这是一个问题,我说它比较慢。 您问慢的原因,我正确解释了慢的原因是基准测试中的缺陷。 不,您说基准正在衡量一些无用的衡量标准。这是真的,但它不会使结果无效。也许问题在于 OSX 机器的上下文切换要慢得多,但这不是库问题。 当然它们是合理的和预期的。我希望有人可以指出 libc++ 实现中的某些内容并说“看,它正在做这个额外的事情,这就是为什么”,也许有人会,但你的回答仍然含糊不清且无济于事。【参考方案2】:

您需要在两个系统上使用相同的 STL 实现。这可能是 libc++ 或 pthread_mutex_*() 中的问题。

然而,其他发帖者所说的互斥锁在 OS X 上是传统的,完全是谎言。是的,马赫锁集和信号量需要对每个操作进行系统调用。但除非您明确使用 lockset 或 semaphore Mach API,否则这些 API 不会在您的应用程序中使用。

OS X 的 libpthread 使用 __psynch_* BSD 系统调用,远程匹配 Linux futexes。在无竞争的情况下,libpthread 不进行系统调用来获取互斥锁。仅使用 cmpxchg 等指令。

来源:libpthread源码和我自己的知识(我是Darling的开发者)。

【讨论】:

【参考方案3】:

David Schwartz 基本上是正确的,除了吞吐量/适应性评论。它在 Linux 上实际上要快得多,因为它使用 futex 并且竞争调用的开销要小得多。这意味着在无竞争的情况下,它只是进行函数调用、原子操作和返回。如果您的大多数锁都是非竞争的(这通常是您在许多实际程序中看到的典型行为),那么获取锁基本上是免费的。即使在有争议的情况下,它基本上也是一个函数调用,系统调用 + 原子操作 + 将 1 个线程添加到列表中(系统调用是操作中昂贵的部分)。如果在系统调用期间释放了互斥锁,则函数会立即返回,而不会进入等待列表。

在 OSX 上,没有 futex。获取互斥体需要始终与内核对话。此外,OSX 是一个微内核混合体。这意味着要与内核对话,您需要向它发送消息。这意味着您进行数据编组、系统调用、将数据复制到单独的缓冲区。然后在某个时候内核来了,解组数据并获取锁并向您发送一条消息。因此,在无争议的情况下,它的重量很多。在有争议的情况下,这取决于您在等待锁定时被阻塞了多长时间:等待的时间越长,在整个运行时摊销时,锁定操作的成本就越低。

在 OSX 上,有一种速度更快的机制,称为调度队列,但它需要重新考虑程序的工作方式。除了使用无锁同步(即无竞争的情况永远不会跳转到内核)之外,它们还进行线程池和作业调度。此外,它们还提供异步调度,让您无需等待锁定即可安排作业。

【讨论】:

在 OS X 上获取互斥锁总是需要内核调用是不正确的。如果您检查了源代码,您会注意到在进行 _psynch* 系统调用之前总是尝试进行原子交换,这或多或少与 futexes 或 Linux 匹配。 OS X 是一个微内核也是不正确的。仅仅因为他们将一些 Mach 代码放入 XNU 并不能使其成为一体。最后,消息发送仅涉及 Mach 服务器调用 - 即不涉及诸如 _psynch* 之类的 BSD 系统调用或诸如 semaphore_wait 之类的 Mach 系统调用。

以上是关于为啥 OSX 上的 std::mutex 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥将 std::mutex 引入成员类会产生此编译错误?

为啥在 Chrome 上的 for 循环中使用 let 这么慢?

为啥使用 std::mutex 的函数对 pthread_key_create 的地址进行空检查?

为啥 std::mutex 在带有 WIndows SOCKET 的结构中使用时会创建 C2248?

为啥带有 SourceTree 的 WSL2 上的 Git 对我来说这么慢?

为啥从相机缩小 UIImage 这么慢?