无竞争线程锁的实际成本

Practical Cost of Uncontended Thread Locks

首先,我应该强调本主题中_完全无竞争的_线程锁。我很清楚线程进入竞争激烈的锁、被阻塞和挂起以及不得不等待轮到它们进行上下文切换等的巨大成本。所以我对竞争锁的成本不太感兴趣。

"Cheap"?

我一再被告知,线程锁在无竞争时是 "really cheap",但在分析时发现实际上恰恰相反。我想这取决于我们如何定义 "really cheap".

所以我的要求是详细信息,可以帮助我理解进入和退出无竞争线程锁的成本在更绝对的术语中,例如各种类型锁的时钟周期 运行ges(可能有点理论上),如果它涉及内存访问和缓存等。我是一个低级编码器,但不完全在 machine/assembly 级别(试图尽可能多地提高我的知识)。

例如,成本是否与使用通用分配器的堆分配相当?有些人认为这很便宜,但我认为它是最昂贵的东西之一。它与 b运行ch 错误预测相当吗?它是否与内存负载的情况有很大的不同,内存负载从缓存行可能非常便宜,但对于完全未缓存的 DRAM 访问却非常昂贵?

作为序言,我想明确表示,我提出这个问题并不是有先见之明,而是试图沉迷于我尚未衡量的事物的微观效率。相反,我在 hindsight 中问这个问题,我在大规模生产代码库中工作了多年,在这些代码库中,我经常用头撞到无竞争的线程锁,实际情况比我更频繁会期望,一个主要的热点。所以我想从更绝对和准确的意义上更好地理解性能,特别是帮助我在设计决策方面更好地与成本相关。

此外,我对构成 "cheap" 的标准可能相当高,因为我通常是在数据结构内部工作的人。例如,许多人似乎认为堆分配相对便宜,如果我们为整个数据结构分配句柄,我会同意。如果我们在数据结构中并为我们插入的每个元素支付开销,它会变得非常昂贵。所以我对"expensive"和"cheap"的看法可能完全不同。

奇怪的代码

我工作的其中一个代码库有很长的历史(几十年)。所以它主要被设计成只能在单线程上工作,有很多实践使得很多基本函数都不是线程安全的(通常甚至不是 reent运行t)。那里一些更有野心的开发人员希望以改进的方式使该代码库越来越多线程化,当然我们 运行 遇到了许多可怕的问题。团队响应:随着 bug 的涌入,到处撒上线程锁。

我是当时为数不多的使用分析器的人之一,并且经常 运行 进入围绕线程锁的热点,线程锁仍然只在完全单线程、无竞争的上下文中使用。最初,代码库为其使用特定于平台的代码,鉴于我主要使用 Windows 作为 development/testing/profiling,锁是 windows API 使用的本机临界区.后来我们开始使用 Qt 来减少可移植性问题,临界区热点在 QMutex 中被瓶颈所取代。后来我们开始整合Intel的一些Thread Building Blocks,我在tbb::mutex看到了一些热点(虽然没有那么多,但我不确定是不是因为我们用的不多还是如果它比前两个解决方案更有效:这是一个 庞大的 代码库,跨越数百万行代码。

这是主要部分。我曾经指出一个主要的瓶颈是 QMutex 锁,它是完全没有竞争的。它仅在单线程上下文中使用,并且锁定只是为了线程安全,以防它曾经在多线程上下文中使用过。所以我的同事 "optimized" 是这样的(伪代码):

if (thread_id != main_thread_id)
     mutex.lock();
...
if (thread_id != main_thread_id)
     mutex.unlock();

这实际上消除了我们的热点并显着提高了性能,足以让报告速度下降的用户对结果非常满意!然而,当我看到那个时,我想我有点吐了。它基于这样一种假设,即这是安全的,因为它在代码中读取只能从主线程修改的资源。

这是我最开始怀疑无竞争线程锁的真正开销的地方,当像上面交换线程 ID 访问和 b运行ching 这样奇怪的代码实际上可以消除重大的现实世界瓶颈时。

所以我的最终问题是,一个无竞争的线程锁到底有多贵(或者至少比 "it's cheap" 更精确)?

在我看到的情况下,如果我凭直觉(完全意识到这可能是完全错误的),我会说我们正在处理的锁 "felt" 就像它们在 100 -循环一种未缓存的 DRAM 访问,如 运行ge(不像 malloc 那样昂贵,但接近那里)。由于人们对 hardware/OS 细节感兴趣,我通常对广泛的答案感兴趣,因为我们一直在处理多平台项目,但也许我的具体兴趣是 x86/x64、Windows、OSX 和 Linux.

FWIW:如果一切都以最佳方式实现,互斥锁可以实现,AFAIR,作为两个原子 increments/decrements(Windows-speak 中的 Interlocked*() 函数);这些依次被翻译(在 x86 上)为带有 LOCK 前缀的 asm 操作,导致总线锁定。

反过来,总线锁定的实现方式完全不同,并且在单路单核、单路多核、带 FSB 的多路和 NUMA/SUMO 机器。但在实践中,我已经看到多路时钟的数字大约为 100 个时钟,而单路时钟的数字为数十个时钟。注意:这些是非常粗略的数字,在您使用 RDTSC 之类的工具执行您自己的测量(准确地在目标硬件上)之前,不要认为它们是理所当然的。

P.S。您提供的代码段(带有 if(thread_id != main_thread_id))可能不安全,即使只能从主线程内修改数据也是如此。