Random Tech Thoughts

The title above is not random

Pthread Mutex,其实它飞快

Update on 2012-12-27: 知道 xchg 实现的 spinlock 竞争严重时变慢的原因后可以对它做一点优化。拿锁失败,发现锁还没有释放时应该尝试等待一段时间而不是不停的去检查,这样就可以避免同时有太多的核进行拿锁操作。优化之后的 xchg spinlock 性能在单个 CPU chip 上明显超过 pthread mutex,核数变多时也还是会胜过一些。加入 exponential backoff 的 spinlock 实现也放在 github 上了。实现简单 spinlock 时 waiter 一定要加 wait backoff,这个技术对 spinlock 的伸缩性影响巨大。


严格的说应该是 pthread mutex 在 Linux 上其实非常快,用户态程序没有特殊需求就不要自己去实现 spinlock 了。

今天受实验室同学的启发,把以前测试 spinlock 性能的 micro benchmark 用 pthread mutex 也试了一下,结果发现这货真是逆天了!

我的 micro benchmark 做的操作非常简单,拿锁、加 counter、然后解锁。不管测试使用多少个核都做 1600 万次这样的操作。

pthread mutex 在无竞争下完成这 1600 万次操作耗时 0.36 秒(Intel Xeon E7 处理器,2 GHz),平均下来每次拿锁、加 counter、解锁操作耗时仅 22.5 纳秒。(如果对这个数字没什么概念,推荐看下 Latency Numbers Every Programmer Should Know

无竞争下 pthread mutex 的开销的确是比 xchg 的实现开销大(0.21 秒),但是只要超过两个核,它的性能就已经超过 xchg,而且核数增多对性能几乎没有影响。到 32 核时,pthread mutex 完成 1600 万次操作总共耗时 1.31 秒,而 xchg 需要 2.82 秒,pthread mutex 快了一倍。要注意的是 xchg 还是实现 spin lock 时伸缩性最好的一个(用户态测试结果)。

这个结果让我有些吃惊。

无竞争下为何速度如此之快?从 stack overflow 上的一个回答来看,Linux 上的 pthread mutex 使用 futex 实现。无竞争时拿锁操作可以完全在用户态完成。

为何竞争激烈时能 pthread mutex 能比 xchg 快如此之多?futex 在发生 contention 时会使用系统调用,调用线程因此可能进入 sleep 状态,从而使得同时尝试拿锁的核数变少。相反,xchg 实现的 spinlock 中,每个 waiter 都在不断的检查锁的状态,lock holder 解锁的写操作会使得所有的 waiter 都发生 cache miss。waiter 很多时,解锁操作使每个 waiter 发生 cache miss,而处理 cache miss message 的硬件(home directory)对这些消息只能一个一个处理,所以同时尝试拿锁的核数越多,拿锁解锁操作变慢越多。实际上,在执行使用 pthread mutex 的 benchmark 时,用 htop 可以看到 CPU 大多数时间都在内核态,而使用 xchg 的 benchmark 运行时则是几乎所有时间都在用户态。

更多关于 cache 对锁性能的巨大影响推荐看 Silas Boyd-Wickizer 的论文 Non-scalable locks are dangerous。(顺带说下,这篇论文的作者中 Frans Kaashoek 是 system research 界的大牛,Robert Morris 就是著名的 morris 蠕虫的作者,跟 Paul Graham 一起创办了 ViaWeb。)

其实 critical section 越短,拿锁解锁操作开销占整个 critical section 执行时间的比例就越长,这时由于竞争导致拿锁解锁操作变慢的影响体现的越明显。我的 micro benchmark 就是这种情况。

这个测试结果给我的启示是,用户态程序想要有良好的伸缩性关键还是好好设计程序结构,避免共享状态导致的竞争。至于同步原语,pthread 提供的实现性能已经足够良好。没有特殊需要自己设计用户态的锁,如果实现不够好反而会导致糟糕的性能。

Comments