首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么在Visualstd::shared_mutex中std::mutex比std::shared_mutex差得多?

为什么在Visualstd::shared_mutex中std::mutex比std::shared_mutex差得多?
EN

Stack Overflow用户
提问于 2021-11-16 13:49:55
回答 1查看 1.1K关注 0票数 12

在发布模式下,在Visual 2022中运行以下命令:

代码语言:javascript
复制
#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";
}

产出如下:

代码语言:javascript
复制
mutex 2.01147s;  shared_mutex 1.32065s
mutex 80 bytes;  shared_mutex 8 bytes

为什么会这样呢?

令人意外的是,更丰富的特性std::shared_mutexstd::mutex更快,后者是其特性中的一个子集。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-11-16 13:49:55

TL;DR:向后兼容性和ABI兼容性问题的不幸组合使std::mutex在下一个ABI崩溃之前变得糟糕。std::shared_mutex是好的。

一个不错的std::mutex实现将尝试使用原子操作来获取锁,如果很忙,可能会尝试在read循环中旋转(在x86上使用一些pause ),最终会求助于OS等待。

有几种方法可以实现这样的std::mutex

  1. 直接委托给相应的OS,这些API完成了上述所有工作。
  2. 自己做旋转和原子操作,只在操作系统等待时调用OS。

当然,第一种方法更容易实现,更易于调试,更健壮。所以这似乎是走的路。候选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::mutexconstexpr的要求,因为运行时检测、放置新的,以及窗口前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实现的虚拟函数调用
  • 最后是Windows调用。

由于使用/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

票数 20
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/69990339

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档