【发布时间】:2019-03-07 14:34:48
【问题描述】:
我想知道与直接执行相比,在 C++11 std::thread(或 std::async)中执行方法的时间开销。我知道线程池可以显着减少甚至完全避免这种开销。但我仍然希望对这些数字有更好的感觉。我想大致了解线程创建的计算成本是多少,池化的成本是多少。
我自己实现了一个简单的基准测试,归结为:
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;
}
这工作得很好,我可以在配备现代 Core i7-6700K 的 Ubuntu 18.04 上获得约 20-80 微秒(us)的典型线程创建成本。一方面,与我的预期相比,这很便宜!
但现在出现了奇怪的部分:线程开销似乎取决于(非常可重现)在有效负载方法中花费的实际时间!这对我来说毫无意义。但它可以在六种不同的硬件机器上重现,这些机器具有各种风格的 Ubuntu 和 CentOS!
- 如果我在
PayloadFunction内部花费 1 到 100us,典型的线程创建成本约为 20us。 - 当我将
PayloadFunction花费的时间增加到100-1000us 时,线程创建成本增加到40us 左右。 -
PayloadFunction进一步增加到 10000us 以上,线程创建成本再次增加到 80us 左右。
我没有去更大的范围,但我可以清楚地看到有效负载时间和线程开销之间的关系(如上所述)。由于我无法解释这种行为,我认为一定有一个陷阱。有没有可能我的时间测量太不准确了?或者 CPU Turbo 是否会根据负载的高低导致不同的时序?有人能解释一下吗?
这是我得到的时间的随机示例。这些数字代表上述模式。在许多不同的计算机硬件(各种 Intel 和 AMD 处理器)和 Linux 风格(Ubuntu 14.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.
【问题讨论】:
-
vThread.join();等待线程终止,因此您通过在前一个线程通知主线程之前不启动新线程来序列化执行。要么分离它,要么创建一个线程数组。 -
哦,我想你是故意的?但这意味着您还要对通知主线程的时间进行基准测试,而不仅仅是创建新线程的时间。
-
@PeterCordes 是的,有意立即加入线程。这个实现创建了线程的串行执行,我可以将负载中花费的净时间与外部时间进行比较,以查看线程创建的开销。增加通知主线程的时间也是有意的,因为我想要线程有效负载执行的总“成本开销”。这个问题不是重复的,我不想分离线程。
-
您的标题说您只是尝试对线程 创建 时间进行微基准测试,但实际上您正在测试整个往返行程以生成单个工作线程并等待其结果,没有并行性。顶部的 TL:DR 总结可能是个好主意。
-
您的 CPU 是否一直固定在 4GHz ?也许与
perf stat核对。也许禁用 turbo 并使用sudo sh -c 'for i in /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference;do echo performance > "$i";done'将您的 EPP 设置为性能。可能不是这样。如果有的话,您会期望更长的工作负载来保持 CPU 以最大速度运行并减少开销。也许缓存会变冷,但额外的 60us 似乎太多了。
标签: linux multithreading performance c++11 benchmarking