首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >执行c++11 std::线程的时间开销是否取决于执行的有效负载?

执行c++11 std::线程的时间开销是否取决于执行的有效负载?
EN

Stack Overflow用户
提问于 2018-10-02 12:55:46
回答 2查看 190关注 0票数 1

与直接执行相比,我想知道在C++11 std::线程(或std::异步)中执行方法的时间开销。我知道线程池可以显著减少甚至完全避免这种开销。但我还是想对这些数字有个更好的感觉。我想大致知道线程创建的计算成本是多少,池的成本是多少。

我自己实现了一个简单的基准,可以归结为:

代码语言:javascript
运行
复制
void PayloadFunction(double* aInnerRuntime, const size_t aNumPayloadRounds) {
    double vComputeValue = 3.14159;

    auto vInnerStart = std::chrono::high_resolution_clock::now();
    for (size_t vIdx = 0; vIdx < aNumPayloadRounds; ++vIdx) {
        vComputeValue = std::exp2(std::log1p(std::cbrt(std::sqrt(std::pow(vComputeValue, 3.14152)))));
    }
    auto vInnerEnd = std::chrono::high_resolution_clock::now();
    *aInnerRuntime += static_cast<std::chrono::duration<double, std::micro>>(vInnerEnd - vInnerStart).count();

    volatile double vResult = vComputeValue;
}

int main() {
    double vInnerRuntime = 0.0;
    double vOuterRuntime = 0.0;

    auto vStart = std::chrono::high_resolution_clock::now();
    for (size_t vIdx = 0; vIdx < 10000; ++vIdx) {
        std::thread vThread(PayloadFunction, &vInnerRuntime, cNumPayloadRounds);
        vThread.join();
    }
    auto vEnd = std::chrono::high_resolution_clock::now();
    vOuterRuntime = static_cast<std::chrono::duration<double, std::micro>>(vEnd - vStart).count();

    // normalize away the robustness iterations:
    vInnerRuntime /= static_cast<double>(cNumRobustnessIterations);
    vOuterRuntime /= static_cast<double>(cNumRobustnessIterations);

    const double vThreadCreationCost = vOuterRuntime - vInnerRuntime;
}

这是很好的工作,我可以得到典型的线程创建成本为20-80微秒(我们)在Ubuntu18.04与一个现代的核心i7-6700 K。一方面,这比我的期望便宜!

但是现在出现了一个奇怪的部分:线程开销似乎取决于(非常可重复的)在有效负载方法中花费的实际时间!这对我来说毫无意义。但是,它的可复制性发生在六台具有不同风格的Ubuntu和CentOS的硬件机器上!

  1. 如果我在PayloadFunction中花费在1到100 is之间,典型的线程创建成本大约是20 is。
  2. 当我将在PayloadFunction中花费的时间增加到100-1000us时,线程创建成本将增加到40 in左右。
  3. PayloadFunction中的线程创建成本进一步增加到超过10000 to,使线程创建成本增加到大约80 to。

我没有进入更大的范围,但我可以清楚地看到负载时间与线程开销之间的关系(如上面所计算的)。由于我无法解释这种行为,我认为一定有一个陷阱。有没有可能我的时间测量如此不准确?或者CPU Turbo是否会基于较高或较低的负载导致不同的时间间隔?有人能给点线索吗?

这里是一个随机的例子,我得到的时间。这些数字代表上述模式。在许多不同的计算机硬件(不同的Intel和AMD处理器)和Linux版本(Ubuntu14.04、16.04、18.04、CentOS 6.9和CentOS 7.4)上都可以看到相同的模式:

代码语言:javascript
运行
复制
payload runtime      0.3 us., thread overhead  31.3 us.
payload runtime      0.6 us., thread overhead  32.3 us.
payload runtime      2.5 us., thread overhead  18.0 us.
payload runtime      1.9 us., thread overhead  21.2 us.
payload runtime      2.5 us., thread overhead  25.6 us.
payload runtime      5.2 us., thread overhead  21.4 us.
payload runtime      8.7 us., thread overhead  16.6 us.
payload runtime     18.5 us., thread overhead  17.6 us.
payload runtime     36.1 us., thread overhead  17.7 us.
payload runtime     73.4 us., thread overhead  22.2 us.
payload runtime    134.9 us., thread overhead  19.6 us.
payload runtime    272.6 us., thread overhead  44.8 us.
payload runtime    543.4 us., thread overhead  65.9 us.
payload runtime   1045.0 us., thread overhead  70.3 us.
payload runtime   2082.2 us., thread overhead  69.9 us.
payload runtime   4160.9 us., thread overhead  76.0 us.
payload runtime   8292.5 us., thread overhead  79.2 us.
payload runtime  16523.0 us., thread overhead  86.9 us.
payload runtime  33017.6 us., thread overhead  85.3 us.
payload runtime  66242.0 us., thread overhead  76.4 us.
payload runtime 132382.4 us., thread overhead  69.1 us.
EN

回答 2

Stack Overflow用户

发布于 2020-08-01 15:27:09

我有一些假设。第一项:

您正在卸载系统上运行此基准,但可能仍然存在低级别的后台活动。然后:

  • 主线程在CPU内核上声明,我将称之为核心1。
  • 主线程触发子线程来运行PayloadFunction
  • 此时,主线程仍在vThread构造函数/syscall中运行,因此子线程被安排在核心2上运行,这是免费的。
  • 稍后,主线程调用join并被挂起。
  • 子线程继续运行在核心2上,核心1仍未被占用。

现在,如果有效负载运行时不太高,那么当子程序退出核心1时,大部分时间仍然是空闲的。主线程唤醒,并由智能系统调度器*在核心1上重新调度。

但有时,当核心1是空闲的,一个随机的背景任务醒来,并被安排到该核心。然后主线程再次苏醒,但核心1仍然被占用。调度程序注意到系统中的core 2或其他内核是免费的,并将主线程迁移到该核心。线程迁移是一种相对昂贵的操作。如果新内核处于休眠状态,则需要发送处理器间中断(或者是内核间中断?)唤醒它。即使这不是必要的,主线程至少会导致减速,因为新核心上的缓存需要加载其数据。我期望新的核心大部分时间都是核心2,因为它刚刚完成了它的子线程,因此现在正在运行调度器,该调度程序刚刚发现主线程可以再次运行。

1a:如果调度程序对它们最后运行的每个线程都记得,并且尝试调度线程在其上运行时再次在同一核心上运行,那么这种情况只取决于主线程醒来时核心1被占用的概率。这种可能性不应在很大程度上取决于核心闲置的时间。但是,如果主线程挂起的时间很短,系统可能没有机会将不同的任务安排到核心1。这在某种程度上与您获得的数据相对应,因为在一个270秒的有效负载运行时附近似乎存在不连续性。

1b:如果调度程序只记得每个内核运行的最后一个线程,并且只有在同一核心上没有运行其他线程时,才尝试在同一核心上再次运行一个线程,那么我们可以期望主线程在核心1上被唤醒的概率与线程休眠的时间成线性关系。然后,每个循环迭代的平均成本将逐渐接近将线程迁移到另一个核心的延迟。

在你的测量中,我认为有太多的抖动强烈地支持上述选项中的一个而不是另一个。

*我不完全确定Windows和Linux在调度线程时有多聪明,因为它们的内核与上次运行的线程相同,但是google的一个快速显示显示,至少有一些调度程序试图做到这一点。下面是是一篇文章,描述了Linux调度程序所做的一些事情,我只快速浏览了一下,但似乎很有趣。

假设二:

当cpu核心由于没有工作可做而进入休眠状态时,它可能会将最后一个进程的进程上下文原封不动地留在上面。我希望它只有在发现要运行一个实际的新过程时才会切换流程上下文。如果core唤醒并发现它可以继续运行以前运行的任务,那么它可能会注意到它不需要更改上下文。

如果上述假设成立,这也意味着线程唤醒并继续运行所需的时间取决于它是否是在核心上运行的最后一个线程,因为在这种情况下,它的上下文(包括内存映射、TLB缓存等)不需要重新加载。当主线程处于休眠状态时,其他东西被调度的概率与线程休眠的时间成线性关系,因此这将显示出类似于假设1b的行为。

测试方法

在上述所有假设下,我们可以预期某些行为:

  • 如果要度量每个单独线程的创建/连接周期的操作,您将看到有一个快速和缓慢的时间,或者可能超过两个级别,对应于如果该线程被迁移了。可能有一种方法可以找出您当前正在运行的物理内核,因此您可以尝试直接验证这一点。
  • 您所测量的减速并不是因为单个线程创建/连接变得更昂贵,而是因为您正在更频繁地采用慢速模式。
  • 您还可以尝试使用cpu关联集来运行您的程序,这样它就只能在一个核心上运行。然后,一个核心将始终被占用,而其他进程将在其他核心上运行。这样就可以消除快速操作和慢操作之间的区别。与当前的快速情况相比,您甚至可能得到加速,因为启动子线程不需要进行内核间的通信。子线程已经启动,但是主线程还没有在join中挂起自己,但我希望这会比处理器间通信延迟少一点。

您可以尝试区分低次幂1a和1b,方法是以较少的抖动进行测量,并试图找出开销的增加是否与两种场景的预期开销相匹配。我不确定您是否能够区分1b和2,但您也可以尝试阅读调度程序。

票数 2
EN

Stack Overflow用户

发布于 2018-10-02 13:06:19

您可能会在计时指令的“错误”方面执行一些代码。避免这种情况的一个简单方法是调用特殊的x86指令CPUID。对于GCC,你可以这样做:

代码语言:javascript
运行
复制
#include <cpuid.h>

unsigned out[4];
__get_cpuid(1, &out[0], &out[1], &out[2], &out[3]);

在你开始计时之前,在你停止计时之后,先打这样的电话。它将起到“围栏”的作用,防止跨越时间边界的操作重新排序。

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

https://stackoverflow.com/questions/52608800

复制
相关文章

相似问题

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