【问题标题】:Why is std::mutex so much worse than std::shared_mutex in Visual C++?为什么 std::mutex 在 Visual C++ 中比 std::shared_mutex 差这么多?
【发布时间】:2021-12-27 14:25:37
【问题描述】:

在 Visual Studio 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_mutexstd::mutex 更快,而std::mutex 严格来说是其功能的一个子集。

【问题讨论】:

  • 我在我的 Windows 1.2 Ghz 笔记本电脑上编写了我自己的与您类似的测量代码,简单的自旋锁在循环中严格工作 24 ns,std::mutex 75-85 ns,std::shared_mutex @ 987654327@.
  • @Arty,比自旋锁慢两倍——是预期的互斥锁性能。您的自旋锁在退出时使用 storememory_order_release,您只需将其释放即可。但是互斥锁会执行一个互锁的获取操作,很可能是exchange,以查看是否需要通知某些等待线程。 (x86 有memory_order_release 的廉价商店,但任何exchange 都不便宜,甚至_relaxed
  • 你看过代码了吗?你的问题到底是什么?您是否比较了两者支持的功能来解释它们的时间差异?或者是尺寸差异,如果您有手柄/身体分离,这根本不稀奇。

标签: c++ visual-c++ stdmutex


【解决方案1】:

TL;DR: 不幸的是,向后兼容性和 ABI 兼容性问题的结合使 std::mutex 在下一次 ABI 中断之前变得糟糕。 OTOH,std::shared_mutex 很好。


std::mutex 的体面实现会尝试使用原子操作来获取锁,如果忙,可能会尝试在读取循环中旋转(在 x86 上使用一些pause),最终将诉诸操作系统等待.

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

  1. 直接委托给执行上述所有操作的相应操作系统 API。
  2. 自行执行旋转和原子操作,仅在操作系统等待时调用操作系统 API。

当然,第一种方式更容易实现,调试更友好,更健壮。所以这似乎是要走的路。候选 API 是:

  • CRITICAL_SECTION API。一个递归互斥体,缺少静态初始化程序,需要显式销毁
  • SRWLOCK。具有静态初始化程序且不需要显式销毁的非递归共享互斥锁
  • WaitOnAddress。等待特定变量更改的 API,类似于 Linux futex

这些原语有操作系统版本要求:

  • CRITICAL_SECTION 存在于我认为 Windows 95 中,尽管 TryEnterCriticalSection 在 Windows 9x 中不存在,但自 Windows Vista 以来添加了使用 CRITICAL_SECTIONCONDITION_VARIABLE 的能力,以及 CONDITION_VARIABLE 本身。
  • SRWLOCK从Windows Vista开始就存在,而TryAcquireSRWLockExclusive从Windows 7开始就存在,所以从Windows 7开始只能直接实现std::mutex
  • WaitOnAddress 从 Windows 8 开始添加。

在添加std::mutex 的时候,需要Visual Studio C++ 库对Windows XP 的支持,所以它是使用自己的服务来实现的。事实上,std::mutex 和其他同步内容已委托给 ConCRT (Concurrency Runtime)

对于 Visual Studio 2015,实现已切换为使用最佳可用机制,即 SRWLOCK 从 Windows 7 开始,CRITICAL_SECTION 在 Windows Vista 中声明。 ConCRT 被证明不是最好的机制,但它仍然用于 Windows XP 和 2003。多态性是通过将具有虚函数的新类放置到由std::mutex 和其他原语提供的缓冲区中来实现的。

请注意,此实现打破了 std::mutexconstexpr 的要求,因为运行时检测、新布局以及 Window 7 之前的实现无法仅具有静态初始化程序。

随着时间的推移,对 Windows XP 的支持最终在 VS 2019 中被删除,对 Windows Vista 的支持在 VS 2022 中被删除,进行更改以避免使用 ConCRT,计划更改甚至避免运行时检测 SRWLOCK(披露: 我贡献了这些 PR)。仍然由于 VS 2015 与 VS 2022 的 ABI 兼容性,无法简化 std::mutex 实现以避免所有这些将类与虚函数一起放置。

更可悲的是,虽然SRWLOCK 有静态初始化器,但所述兼容性阻止了constexpr 互斥锁:我们必须在那里放置新的实现。不可能避免放置新的,并在std::mutex 内部进行构建,因为std::mutex 必须是标准布局类(请参阅Why is std::mutex a standard-layout class?)。

所以大小开销来自于 ConCRT 互斥体的大小。

而运行时开销来自调用链:

  • 调用库函数以获取标准库实现
  • 虚拟函数调用以获取基于 SRWLOCK 的实现
  • 最后是 Windows API 调用。

由于标准库 DLL 是使用 /guard:cf 构建的,因此虚拟函数调用比通常更昂贵。

部分运行时开销是由于std::mutex 填充所有权计数和锁定线程。即使SRWLOCK 不需要此信息。这是由于与recursive_mutex 共享的内部结构。额外的信息可能对调试有帮助,但填写起来确实需要时间。


std::shared_mutex 设计为仅支持启动 Windows 7 的系统。因此它直接使用SRWLOCK

std::shared_mutex 的大小是SRWLOCK 的大小。 SRWLOCK 与指针大小相同(尽管在内部它不是指针)。

它仍然涉及一些可避免的开销:它调用 C++ 运行时库,只是为了调用 Windows API,而不是直接调用 Windows API。不过,这看起来可以通过下一个 ABI 解决。

std::shared_mutex 构造函数可以是 constexpr,因为SRWLOCK 不需要动态初始化器,但标准禁止将constexpr 自愿添加到标准类中。

【讨论】:

  • 因此,在 windows 上将 std 互斥体与 shared 交换是有意义的,并且是相对未来的证明。
  • @Yakk-AdamNevraumont,是的。它可能在未来变得无用,但不太可能变得有害。但是,如果您将其与condition_variable 一起使用,则需要condition_variable_anyshared_mutex 耦合,shared_mutex 没有专门的condition_variable
  • 这是一个非常有启发性的问答 Alex,谢谢!我注意到您在问题中打印了 sizeof 两种互斥锁类型。我在这里猜测,但是shared_mutex 8 的大小是因为它只包含一个指向共享控制块的指针吗?
  • @TedLyngmo,我已经编辑了答案以涵盖该问题。没有共享控制块。 SRWLOCK 本身具有与指针相同的大小(尽管在内部它不是指针)。 shared_mutex 仅按值包含 SRWLOCK
  • TryEnterCriticalSection 并非仅限 W2K。它是NT-only。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-28
  • 1970-01-01
  • 2016-09-07
  • 2022-09-28
  • 1970-01-01
  • 2015-04-18
  • 1970-01-01
相关资源
最近更新 更多