我想知道同样的事情,所以我测量了它。
在我的盒子上(AMD FX(tm)-8150 八核处理器,3.612361 GHz),
锁定和解锁位于其自己的缓存行中且已缓存的未锁定互斥体需要 47 个时钟(13 ns)。
由于两个内核之间的同步(我使用 CPU #0 和 #1),
我只能在两个线程上每 102 ns 调用一次锁定/解锁对,
所以每 51 ns 一次,从中可以得出结论,在一个线程解锁之后,下一个线程可以再次锁定它之前,大约需要 38 ns 才能恢复。
我用来调查这个问题的程序可以在这里找到:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
请注意,它有一些特定于我的盒子的硬编码值(xrange、yrange 和 rdtsc 开销),因此您可能必须先对其进行试验,然后才能使用它。
它在该状态下生成的图形是:
这显示了在以下代码上运行基准测试的结果:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
两个 rdtsc 调用测量锁定和解锁“互斥锁”所需的时钟数(我的盒子上的 rdtsc 调用开销为 39 个时钟)。第三个 asm 是延迟循环。线程 1 的延迟循环大小比线程 0 小 1 个计数,因此线程 1 稍快。
上述函数在大小为 100,000 的紧密循环中调用。尽管线程 1 的函数稍快一些,但由于调用了互斥锁,两个循环都同步了。从图中可以看出这一点,即线程 1 的锁定/解锁对测量的时钟数略大,这是因为它下面的循环中的延迟较短。
在上图中,右下角的点是延迟 loop_count 为 150 的测量值,然后跟随底部的点,向左,每个测量值将 loop_count 减一。当它变为 77 时,该函数在两个线程中每 102 ns 调用一次。如果随后 loop_count 进一步减少,则不再可能同步线程,并且互斥体在大多数情况下实际上开始被锁定,从而导致执行锁定/解锁所需的时钟量增加。函数调用的平均时间也因此增加;所以情节点现在又向上并向右移动了。
由此我们可以得出结论,每 50 ns 锁定和解锁一个互斥锁对我的机器来说不是问题。
总而言之,我的结论是,对 OP 问题的回答是,只要减少争用,添加更多互斥锁会更好。
尝试尽可能短地锁定互斥锁。将它们放在循环之外的唯一原因是,如果该循环的循环速度超过每 100 ns 一次(或者更确切地说,想要同时运行该循环的线程数乘以 50 ns)或 13 ns 时间循环大小比您通过争用获得的延迟更多。
编辑:我现在对这个主题有了更多的了解,并开始怀疑我在这里提出的结论。首先,CPU 0 和 1 是超线程的;尽管 AMD 声称拥有 8 个真正的核心,但肯定有一些非常可疑的东西,因为其他两个核心之间的延迟要大得多(即 0 和 1 形成一对,2 和 3、4 和 5、6 和 7 也是如此) )。其次,std::mutex 的实现方式是,当它无法立即获得互斥锁上的锁(这无疑会非常慢)时,它会在实际执行系统调用之前先旋转一点锁。所以我在这里测量的是绝对最理想的情况,实际上锁定和解锁每次锁定/解锁可能需要更多的时间。
归根结底,互斥锁是用原子实现的。为了在内核之间同步原子,必须锁定内部总线,这会将相应的高速缓存行冻结数百个时钟周期。在无法获得锁的情况下,必须执行系统调用使线程进入休眠状态;这显然非常慢(系统调用大约为 10 微秒)。通常这不是一个真正的问题,因为该线程无论如何都必须休眠 - 但它可能是一个高争用的问题,其中一个线程无法在它正常旋转的时间内获得锁,系统调用也是如此,但 CAN不久之后拿锁。例如,如果多个线程在一个紧密的循环中锁定和解锁一个互斥体,并且每个线程都保持锁定 1 微秒左右,那么它们可能会因为它们不断地进入睡眠状态并再次被唤醒而大大减慢。此外,一旦一个线程休眠并且另一个线程必须唤醒它,该线程必须进行系统调用并延迟约 10 微秒;因此,当另一个线程在内核中等待该互斥体时(在旋转花费了太长时间之后),解锁互斥体时会发生这种延迟。