异步的令人困惑的行为

Puzzling behaviour of async

本文关键字:异步      更新时间:2023-10-16

这可能是一些奇怪的Linux怪癖,但我观察到了非常奇怪的行为。

下面的代码应该将数字求和的同步版本与异步版本进行比较。问题是,我看到了性能的提高(它不是缓存,即使我将代码拆分为两个单独的程序也会发生这种情况),同时仍然将程序视为单线程(只使用了一个核心)。

strace确实显示了一些线程活动,但像top克隆这样的监视工具仍然只显示一个已使用的核心。

我观察到的第二个问题是,如果我增加派生比率,内存使用量就会爆炸式增长。线程的内存开销是多少?使用5000个线程,我可以使用大约10GB的内存。

#include <iostream>
#include <random>
#include <chrono>
#include <future>
using namespace std;

long long sum2(const vector<int>& v, size_t from, size_t to)
{
const size_t boundary = 5*1000*1000;
if (to-from <= boundary)
{
long long rsum = 0;
for (;from < to; from++)
{
rsum += v[from];
}
return rsum;
}
else
{
size_t mid = from + (to-from)/2;
auto s2 = async(launch::async,sum2,cref(v),mid,to);
long long rsum = sum2(v,from,mid);
rsum += s2.get();
return rsum;
}
}
long long sum2(const vector<int>& v)
{
return sum2(v,0,v.size());
}
long long sum(const vector<int>& v)
{
long long rsum = 0;
for (auto i : v)
{
rsum += i;
}
return rsum;
}
int main()
{
const size_t vsize = 100*1000*1000;
vector<int> x;
x.reserve(vsize);
mt19937 rng;
rng.seed(chrono::system_clock::to_time_t(chrono::system_clock::now()));
uniform_int_distribution<uint32_t> dist(0,10);
for (auto i = 0; i < vsize; i++)
{
x.push_back(dist(rng));
}
auto start = chrono::high_resolution_clock::now();
long long suma = sum(x);
auto end = chrono::high_resolution_clock::now();
cout << "Sum is " << suma << endl;
cout << "Duration " << chrono::duration_cast<chrono::nanoseconds>(end - start).count() << " nanoseconds." << endl;
start = chrono::high_resolution_clock::now();
suma = sum2(x);
end = chrono::high_resolution_clock::now();
cout << "Async sum is " << suma << endl;
cout << "Async duration " << chrono::duration_cast<chrono::nanoseconds>(end - start).count() << " nanoseconds." << endl;
return 0;
}

也许您观察到使用了一个核心,因为同时工作的线程之间的重叠太短而不明显。在现代硬件上,从连续的内存区域求和5mln值应该非常快,所以当父母完成求和时,孩子可能刚刚开始,父母可能会花费大部分或全部时间等待孩子的结果。你有没有试着增加工作单位,看看重叠是否变得明显?

关于提高性能:即使由于工作单元太小,线程之间有0个重叠,多线程版本仍然可以从额外的一级缓存中受益。对于这样的测试,内存可能是一个瓶颈,顺序版本将只使用一个一级缓存,而多线程版本将使用尽可能多的内核。

您检查了正在打印的时间吗?在我的机器上,串行时间在-O2下不到1秒,而并行求和时间快几倍。因此,完全有可能CPU的使用时间不够长,无法注册"top"之类的东西,因为它们通常每秒只刷新一次。

如果通过减少每个线程的计数来增加线程数量,那么就有效地增加了线程管理的开销。如果有5000个线程处于活动状态,那么您的任务将占用额外内存中5000*分钟的线程堆栈大小。在我的机器上,它是20Gb!

为什么不尝试增加源容器的大小?如果您使并行部分花费足够长的时间,您将看到相应的并行CPU使用情况。然而,请做好准备:整数求和速度很快,生成随机数所需的时间可能比将这些数字相加所用的时间长一两个数量级。