在发布模式下,在Visual 2022中运行以下命令:
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <iostream>
std::mutex mx;
std::shared_mutex smx;
constexpr int N = 100'000'000;
int main()
{
auto t1 = std::chrono::steady_clock::now();
for (int i = 0; i != N; i++)
{
std::unique_lock<std::mutex> l{ mx };
}
auto t2 = std::chrono::steady_clock::now();
for (int i = 0; i != N; i++)
{
std::unique_lock<std::shared_mutex> l{ smx };
}
auto t3 = std::chrono::steady_clock::now();
auto d1 = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
auto d2 = std::chrono::duration_cast<std::chrono::duration<double>>(t3 - t2);
std::cout << "mutex " << d1.count() << "s; shared_mutex " << d2.count() << "s\n";
std::cout << "mutex " << sizeof(mx) << " bytes; shared_mutex " << sizeof(smx) << " bytes \n";
}产出如下:
mutex 2.01147s; shared_mutex 1.32065s
mutex 80 bytes; shared_mutex 8 bytes为什么会这样呢?
令人意外的是,更丰富的特性std::shared_mutex比std::mutex更快,后者是其特性中的一个子集。
发布于 2021-11-16 13:49:55
TL;DR:向后兼容性和ABI兼容性问题的不幸组合使std::mutex在下一个ABI崩溃之前变得糟糕。std::shared_mutex是好的。
一个不错的std::mutex实现将尝试使用原子操作来获取锁,如果很忙,可能会尝试在read循环中旋转(在x86上使用一些pause ),最终会求助于OS等待。
有几种方法可以实现这样的std::mutex
当然,第一种方法更容易实现,更易于调试,更健壮。所以这似乎是走的路。候选API是:
CRITICAL_SECTION API。递归互斥,缺乏静态初始化程序,需要显式销毁。SRWLOCK。非递归共享互斥体,具有静态初始化程序,不需要显式销毁。WaitOnAddress。等待要更改的特定变量的API,类似于Linux futex。这些原语有操作系统版本要求:
CRITICAL_SECTION存在于我认为Windows 95之后,虽然TryEnterCriticalSection不在Windows9x中,但与CONDITION_VARIABLE一起使用CRITICAL_SECTION的能力是在Windows和CONDITION_VARIABLE本身之后添加的。SRWLOCK存在于Windows之后,而TryAcquireSRWLockExclusive存在于Windows7之后,因此它只能直接实现从Windows7开始的std::mutex。WaitOnAddress从Windows 8开始添加。在添加std::mutex时,需要Visual C++库对Windows的支持,因此它是使用自行操作实现的。事实上,std::mutex和其他同步内容被委托给ConCRT (并发运行时)。
对于Visual 2015,实现被切换为使用最佳可用机制,即从Windows7开始的SRWLOCK和在Windows中声明的CRITICAL_SECTION。ConCRT被证明不是最好的机制,但它仍然被用于Windows和2003年。这种多态性是通过将具有虚拟函数的类放置到std::mutex和其他原语提供的缓冲区中来实现的。
请注意,此实现打破了std::mutex为constexpr的要求,因为运行时检测、放置新的,以及窗口前7实现不能只有静态初始化器。
随着时间的推移,对Windows的支持最终在VS 209中被放弃,对Windows的支持在VS 2022中被删除,更改是为了避免ConCRT的使用,更改计划甚至是为了避免运行时检测SRWLOCK (公开:我已经贡献了这些PRs)。尽管如此,由于VS 2015的ABI兼容性,所以不可能简化std::mutex实现以避免使用虚拟函数的所有这些类。
更可悲的是,尽管SRWLOCK有静态初始化程序,但是上述兼容性阻止了constexpr互斥:我们必须在那里放置新的实现。不可能避免放置新的位置,并在std::mutex内部构造一个实现,因为std::mutex必须是标准的布局类(参见为什么std::mutex是一个标准布局类?)。
因此,大小开销来自于ConCRT互斥体的大小。
运行时开销来自调用链:
SRWLOCK-based实现的虚拟函数调用由于使用/guard:cf构建标准库DLL,虚拟函数调用比通常更昂贵。
运行时开销的某些部分是由std::mutex填充所有权计数和锁定线程造成的。尽管SRWLOCK不需要这些信息。这是由于与recursive_mutex共享的内部结构。额外的信息可能有助于调试,但确实需要时间来填写。
std::shared_mutex只支持启动Windows7的系统,所以它直接使用SRWLOCK。
std::shared_mutex的大小等于SRWLOCK的大小。SRWLOCK的大小与指针相同(尽管在内部它不是指针)。
它仍然涉及一些可以避免的开销:它调用C++运行时库,只是为了调用Windows,而不是直接调用Windows。不过,下一个ABI似乎解决了这个问题。
std::shared_mutex构造函数可以是SRWLOCK,因为SRWLOCK不需要动态初始化器,但标准禁止向标准类自愿添加constexpr。
https://stackoverflow.com/questions/69990339
复制相似问题