无用螺纹锁的实际成本

时间:2015-05-29 11:30:44

标签: c++ multithreading performance thread-safety

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

"廉价"吗

我一再被告知线程锁是"非常便宜" 在无竞争时,只是在分析时发现相反的情况。我想这取决于我们如何定义"非常便宜"

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

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

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

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

奇怪的代码

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

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

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

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

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

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

所以我的最终问题是,完全无争用的线程锁定是多么昂贵(或者至少比#34更便宜?它很便宜")

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

1 个答案:

答案 0 :(得分:5)

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

反过来,总线锁的实现方式完全不同,MIGHT在单插槽单核,单插槽多核,多插槽FSB和NUMA / SUMO机器上的表现完全不同。但实际上,我已经看到多插座的数字大约为100个时钟,单插座的时钟数量为几十个。注意:这些是非常粗略的数字,除非您使用像RDTSC这样的东西执行自己的测量(在目标硬件上),否则不要将它们视为合格。

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