无用螺纹锁的实际成本

Dra*_*rgy 3 c++ performance multithreading thread-safety

首先,我应该在这个主题中强调_completely uncontended_ thread lock.我非常清楚线程进入高度争用的锁定,被阻止和暂停以及必须等待轮到他们的上下文切换等等.所以我对争用锁的成本并不十分感兴趣.

"廉价"?

我一再被告知螺纹锁在非竞争时"非常便宜",但在实际剖析时却发现相反.我想这取决于我们如何定义"非常便宜".

因此,我的请求是为了让我了解以更多绝对术语进入和退出无竞争线程锁的成本,例如各种类型锁的时钟周期范围(可能有些理论上),如果它与内存访问和我是一个低级编码器,但不是机器/组件级别(试图尽我所能提高我的知识).

例如,成本是否与使用通用分配器的堆分配相当?有些人认为价格便宜,但我认为它是最昂贵的东西之一.它是否与分支错误预测相当?它是否与内存负载的情况有很大差异,从高速缓存线可能超级便宜,但完全未缓存的DRAM访问相当昂贵?

作为序言,我想明确表示,我不是在先见之明中提出这个问题,而是试图过度关注我尚未衡量的微观效率.相反,我在事后的工作中花了很多年时间在大规模的生产代码库中我经常问这个问题,在这些代码库中,我经常遇到无争议的线程锁定,实际存在,远比我预期的更为常见,这是一个主要的热点.因此,我希望以更加绝对和准确的方式更好地理解性能,尤其是帮助我在设计决策方面更好地与成本相关联.

我对"廉价"构成的标准也可能相当高,因为我通常是在数据结构中工作的人.例如,许多人似乎认为堆分配相对便宜,我同意我们是否将句柄分配给整个数据结构.如果我们在数据结构中并为我们插入的每个元素支付了这些开销,那么它可能会非常昂贵.所以我的"昂贵"和"便宜"的想法可能会大不相同.

奇怪的代码

我工作过的代码库之一有很长的遗产(几十年).所以它主要设计为仅使用单线程进行大量实践,这些实践甚至使许多基本功能都不是线程安全的(通常甚至不是可重入的).一些更雄心勃勃的开发人员希望以改进的方式使代码库越来越多线程化,当然我们遇到了许多可怕的问题.团队响应:当虫子涌入时,在整个地方撒上线程锁.

我是当时为数不多的使用分析器的人之一,并且经常遇到围绕线程锁的热点,这些线程锁仍然只在完全单线程,无竞争的环境中使用.最初代码库使用特定于平台的代码,并且考虑到我主要使用Windows进行开发/测试/分析,这些锁是Windows API使用的本机关键部分.后来我们开始使用Qt来减少可移植性问题,并且关键部分热点被瓶颈所取代QMutex.然后我们开始整合一些英特尔的线程构建模块,我看到了一些热点tbb::mutex(虽然不是很多,但我不确定是不是因为我们没有那么多使用它,或者它是否比它更有效前两个解决方案:这是一个跨越数百万行代码的庞大代码库.

这是最重要的部分.我曾经指出一个QMutex锁定的主要瓶颈是完全无条件的.它仅用于单线程上下文,并且锁定仅用于线程安全,以防它在多线程上下文中使用.所以我的同事"优化"了它(伪代码):

if (thread_id != main_thread_id)
     mutex.lock();
...
if (thread_id != main_thread_id)
     mutex.unlock();
Run Code Online (Sandbox Code Playgroud)

这实际上消除了我们的热点并显着提高了性能,足以让报告减速的用户对结果非常满意!但是当我看到它时,我想我嘴里吐了一口气.它基于这样的假设:这是安全的,因为它是在代码中读取只能从主线程修改的资源.

这是我最开始怀疑无争用线程锁的真正代价的地方,当代码像上面的交换线程ID访问和分支一样奇怪,实际上可以消除重大的现实瓶颈.

所以我的最终问题是,无条件的线程锁是多么昂贵(或者至少比"它便宜"更精确)?

在我看到的情况下,如果我凭直觉(完全意识到它可能完全错误),我会说我们处理的锁"感觉"就像他们在100周期的未缓存DRAM访问类似范围(不像malloc那么昂贵,但接近那里).由于人们对硬件/操作系统的细节很感兴趣,我一般都对广泛的答案感兴趣,因为我们总是处理多平台项目,但也许我特别感兴趣的是x86/x64,Windows,OSX和Linux.

No-*_*are 5

FWIW:如果一切都以最佳方式实现,可以实现互斥,AFAIR,两个原子递增/递减(Windows中的Interlocked*()函数); 这些转换(在x86上)转换为带有LOCK前缀的asm操作,导致总线锁定.

反过来,总线锁的实现方式完全不同,MIGHT在单插槽单核,单插槽多核,多插槽FSB和NUMA/SUMO机器上的表现完全不同.但实际上,我已经看到数字大约100个时钟用于多插槽,而数十个时钟用于单插槽.注意:这些是非常粗略的数字,在使用像RDTSC之类的东西执行自己的测量(在目标硬件上)之前,不要认为它们是理所当然的.

PS您提供的代码段(使用if(thread_id!= main_thread_id))可能不安全,即使数据只能在主线程内修改.

  • 在x86上,据我所知,RDTSC是查找时钟的最佳方法.注意事项:在某些多插槽机器上,RDTSC可能具有很大的误导性(如果没有通过主板正确初始化,RDTSC计数器将不会在不同的插槽之间完全同步). (2认同)