与直接执行相比,我想知道在C++11 std::线程(或std::异步)中执行方法的时间开销。我知道线程池可以显著减少甚至完全避免这种开销。但我还是想对这些数字有个更好的感觉。我想大致知道线程创建的计算成本是多少,池的成本是多少。
我自己实现了一个简单的基准,可以归结为:
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的硬件机器上!
PayloadFunction
中花费在1到100 is之间,典型的线程创建成本大约是20 is。PayloadFunction
中花费的时间增加到100-1000us时,线程创建成本将增加到40 in左右。PayloadFunction
中的线程创建成本进一步增加到超过10000 to,使线程创建成本增加到大约80 to。我没有进入更大的范围,但我可以清楚地看到负载时间与线程开销之间的关系(如上面所计算的)。由于我无法解释这种行为,我认为一定有一个陷阱。有没有可能我的时间测量如此不准确?或者CPU Turbo是否会基于较高或较低的负载导致不同的时间间隔?有人能给点线索吗?
这里是一个随机的例子,我得到的时间。这些数字代表上述模式。在许多不同的计算机硬件(不同的Intel和AMD处理器)和Linux版本(Ubuntu14.04、16.04、18.04、CentOS 6.9和CentOS 7.4)上都可以看到相同的模式:
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.
发布于 2020-08-01 15:27:09
我有一些假设。第一项:
您正在卸载系统上运行此基准,但可能仍然存在低级别的后台活动。然后:
PayloadFunction
。vThread
构造函数/syscall中运行,因此子线程被安排在核心2上运行,这是免费的。join
并被挂起。现在,如果有效负载运行时不太高,那么当子程序退出核心1时,大部分时间仍然是空闲的。主线程唤醒,并由智能系统调度器*在核心1上重新调度。
但有时,当核心1是空闲的,一个随机的背景任务醒来,并被安排到该核心。然后主线程再次苏醒,但核心1仍然被占用。调度程序注意到系统中的core 2或其他内核是免费的,并将主线程迁移到该核心。线程迁移是一种相对昂贵的操作。如果新内核处于休眠状态,则需要发送处理器间中断(或者是内核间中断?)唤醒它。即使这不是必要的,主线程至少会导致减速,因为新核心上的缓存需要加载其数据。我期望新的核心大部分时间都是核心2,因为它刚刚完成了它的子线程,因此现在正在运行调度器,该调度程序刚刚发现主线程可以再次运行。
1a:如果调度程序对它们最后运行的每个线程都记得,并且尝试调度线程在其上运行时再次在同一核心上运行,那么这种情况只取决于主线程醒来时核心1被占用的概率。这种可能性不应在很大程度上取决于核心闲置的时间。但是,如果主线程挂起的时间很短,系统可能没有机会将不同的任务安排到核心1。这在某种程度上与您获得的数据相对应,因为在一个270秒的有效负载运行时附近似乎存在不连续性。
1b:如果调度程序只记得每个内核运行的最后一个线程,并且只有在同一核心上没有运行其他线程时,才尝试在同一核心上再次运行一个线程,那么我们可以期望主线程在核心1上被唤醒的概率与线程休眠的时间成线性关系。然后,每个循环迭代的平均成本将逐渐接近将线程迁移到另一个核心的延迟。
在你的测量中,我认为有太多的抖动强烈地支持上述选项中的一个而不是另一个。
*我不完全确定Windows和Linux在调度线程时有多聪明,因为它们的内核与上次运行的线程相同,但是google的一个快速显示显示,至少有一些调度程序试图做到这一点。下面是是一篇文章,描述了Linux调度程序所做的一些事情,我只快速浏览了一下,但似乎很有趣。
假设二:
当cpu核心由于没有工作可做而进入休眠状态时,它可能会将最后一个进程的进程上下文原封不动地留在上面。我希望它只有在发现要运行一个实际的新过程时才会切换流程上下文。如果core唤醒并发现它可以继续运行以前运行的任务,那么它可能会注意到它不需要更改上下文。
如果上述假设成立,这也意味着线程唤醒并继续运行所需的时间取决于它是否是在核心上运行的最后一个线程,因为在这种情况下,它的上下文(包括内存映射、TLB缓存等)不需要重新加载。当主线程处于休眠状态时,其他东西被调度的概率与线程休眠的时间成线性关系,因此这将显示出类似于假设1b的行为。
测试方法
在上述所有假设下,我们可以预期某些行为:
join
中挂起自己,但我希望这会比处理器间通信延迟少一点。您可以尝试区分低次幂1a和1b,方法是以较少的抖动进行测量,并试图找出开销的增加是否与两种场景的预期开销相匹配。我不确定您是否能够区分1b和2,但您也可以尝试阅读调度程序。
发布于 2018-10-02 13:06:19
您可能会在计时指令的“错误”方面执行一些代码。避免这种情况的一个简单方法是调用特殊的x86指令CPUID。对于GCC,你可以这样做:
#include <cpuid.h>
unsigned out[4];
__get_cpuid(1, &out[0], &out[1], &out[2], &out[3]);
在你开始计时之前,在你停止计时之后,先打这样的电话。它将起到“围栏”的作用,防止跨越时间边界的操作重新排序。
https://stackoverflow.com/questions/52608800
复制相似问题