gpt4 book ai didi

c++ - 是什么让boost::shared_mutex如此缓慢

转载 作者:太空狗 更新时间:2023-10-29 20:19:45 24 4
gpt4 key购买 nike

我使用Google基准测试进行了以下3个测试,结果使我感到惊讶,因为RW锁定比 Release模式下的简单互斥锁慢约4倍。 (在 Debug模式下,速度比简单互斥锁慢约10倍)

void raw_access() {
(void) (gp->a + gp->b);
}

void mutex_access() {
std::lock_guard<std::mutex> guard(g_mutex);
(void) (gp->a + gp->b);
}

void rw_mutex_access() {
boost::shared_lock<boost::shared_mutex> l(g_rw_mutex);
(void) (gp->a + gp->b);
}

结果是:
2019-06-26 08:30:45
Running ./perf
Run on (4 X 2500 MHz CPU s)
CPU Caches:
L1 Data 32K (x2)
L1 Instruction 32K (x2)
L2 Unified 262K (x2)
L3 Unified 4194K (x1)
Load Average: 5.35, 3.22, 2.57
-----------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------
BM_RawAccess 1.01 ns 1.01 ns 681922241
BM_MutexAccess 18.2 ns 18.2 ns 38479510
BM_RWMutexAccess 92.8 ns 92.8 ns 7561437

我没有通过Google获得足够的信息,因此希望在这里有所帮助。

谢谢

最佳答案

我不知道标准库/boost/etc如何的细节。尽管标准库版本看起来更快(恭喜,无论谁写的),实现方式都不同。

因此,我将尝试在理论层面上解释各种互斥量类型之间的速度差异,这将解释为什么共享的互斥量(应该)较慢。

原子自旋锁

此外,作为一项学术练习,请考虑最简单的线程安全的“类似于互斥体”的实现:简单的原子自旋锁。

本质上,这不过是std::atomic<bool>std::atomic_flag。它被初始化为false。要“锁定”互斥锁,您只需在循环中执行原子比较和交换操作,直到获得错误的值(即,在将其原子设置为true之前,先前的值为false)。

std::atomic_flag flag = ATOMIC_FLAG_INIT;

// lock it by looping until we observe a false value
while (flag.test_and_set()) ;

// do stuff under "mutex" lock

// unlock by setting it back to false state
flag.clear();

但是,由于这种构造的性质,我们称之为“不公平”的互斥锁,因为获取锁的线程顺序不一定是他们开始尝试锁定锁的顺序。也就是说,在争用较高的情况下,线程可能会尝试锁定而永远不会成功,因为其他线程会更幸运。这对时间非常敏感。想象一下音乐椅。

因此,尽管它的功能像互斥锁,但它不是我们认为的“互斥锁”。

互斥体

互斥锁可以认为是建立在原子自旋锁之上的(尽管通常不这样实现,因为互斥锁通常是在操作系统和/或硬件的支持下实现的)。

本质上,互斥锁比原子自旋锁高出一步,因为它有一个等待线程队列。这使它变得“公平”,因为锁定获取的顺序(或多或少)与锁定尝试的顺序相同。

如果您已经注意到,如果您运行 sizeof(std::mutex),它可能会比您预期的要大一些。在我的平台上是40字节。该多余的空间用于保存状态信息,特别是包括用于访问每个互斥量的锁定队列的某种方式。

当您尝试锁定互斥锁时,它会执行一些低级线程安全操作以对互斥锁的状态信息进行线程安全访问(例如,原子自旋锁),检查互斥锁的状态,并将您的线程添加到锁定队列中,并且(通常)使线程在等待时进入休眠状态,这样就不会浪费宝贵的CPU时间。在线程进入休眠的同时,原子地释放低级线程安全操作(例如,原子自旋锁)(通常,这是有效地需要OS或硬件支持的地方)。

解锁是通过执行低级线程安全操作(例如原子自旋锁),从队列中弹出下一个等待线程并唤醒它来执行的。现在,已唤醒的线程“拥有”该锁。漂洗并重复。

共享互斥

共享互斥锁使这一概念更进一步。它可以由单个线程拥有读/写权限(如普通的互斥锁),也可以由多个线程拥有只读权限(显然,无论如何,这取决于程序员,以确保其安全性)。

因此,除了唯一的所有权队列(如普通的互斥锁)之外,它还具有共享的所有权状态。共享所有权状态可以简单地是当前具有共享所有权的线程数的计数。如果您检查 sizeof(std::shared_mutex),您会发现它通常甚至比 std::mutex大。例如,在我的系统上,它是56个字节。

因此,当您锁定共享互斥锁时,它必须执行普通互斥锁所做的所有事情,而且还必须验证其他一些内容。例如,如果您尝试唯一锁定,则必须验证没有共享所有者。当您尝试共享锁定时,它必须验证没有唯一的所有者。

因为我们通常希望互斥锁是“公平的”,所以一旦队列中有唯一的储物柜,即使将来共享锁可能正在由多个线程共享(即非唯一)锁,也必须将 future 的共享锁尝试排入队列,而不是获取锁。 。这是为了确保共享所有者不会“欺负”想要唯一所有权的线程。

但这又是另一回事:排队逻辑必须确保在共享所有权期间,共享储物柜绝不放置在空队列中(因为它应该立即成功并成为另一个共享所有者)。

此外,如果有一个唯一的储物柜,然后是一个共享的储物柜,再是一个唯一的储物柜,则它必须(大致)保证获得顺序。因此,锁定队列中的每个条目还需要一个表示其用途的标志(即共享与唯一)。

然后我们想到唤醒逻辑。解锁共享互斥锁时,逻辑会因互斥锁的当前所有权类型而异。如果解锁线程具有唯一所有权或是最后一个共享所有者,则它可能必须从队列中唤醒某些线程。它将唤醒队列中请求共享所有权的所有线程,或者唤醒队列中请求唯一所有权的单个线程。

正如您可以想象的那样,所有这些额外的逻辑都是关于谁由于什么原因以及如何更改而锁定的,这不仅取决于当前所有者,而且取决于队列的内容,这可能会使其速度变慢。希望您阅读的频率明显高于写作,因此可以让许多共享所有者同时运行,从而减轻了协调所有这些的性能影响。

关于c++ - 是什么让boost::shared_mutex如此缓慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56763706/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com