[求助]请教一个 C++多线程的性能问题

11 小时 46 分钟前
 Ainokiseki
LZ 受命使用多线程提高某个程序的运行效率。原来程序的行为大概是 step0,step1,step2 之后回到 step0 ,循环若干次。LZ 把 step1 和 step2 的一部分流程并行化,开了三个后台线程做消费者,主线程把任务放在队列里面,消费者线程拿取之后运行,运行完毕后主线程继续。代码如下:

```
std::vector<std::future<void>>futures;
for(auto npg:m_NPGs){
futures.emplace_back(ThreadPoolSingleton::getNPCInstance(threadNum.getValue()).enqueue([this,npg]() {
npg->cycleStep1();
}));
}

for(auto &k:futures){
k.wait();
}
```
线程池代码如下:
```
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for(size_t i = 0;i<threads;++i)
workers.emplace_back(
[this]
{
for(;;)
{
std::function<void()> task;

{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}

task();
}
}
);
}
```
然而出乎楼主预料的是,使用多线程之后性能不增反降,进一步分析发现没有使用多线程优化的 step0 的用时相较之前有着明显增加,幅度达到了单线程时的两倍。火焰图上看,step0 期间调用的每个函数用时增长幅度大致相当。

单线程时,用时分别为:t0=2s, t1=5.58s, t2=10s 多线程时,用时为:t0=3.97s t1=9.3s t2=10.4s

楼主现在不理解的是,step0 看起来和我的多线程优化没有任何关系,在 step0 运行时,三个后台线程应该都在被 condition_variable 阻塞。为什么用时会增加呢?
楼主运行环境为多人共用的 linux 服务器,100 多个空闲核心,开三个后台线程应该不会有什么影响。
1007 次点击
所在节点    C++
19 条回复
AnroZ
11 小时 23 分钟前
step0 干了什么?代码没看到,没办法帮忙分析。
单独开个三个后台线程没什么问题,但每个人都这么想会有问题,应该有资源分配限制吧?
RinGress
11 小时 13 分钟前
没代码不好说。
能想到的是核间通信延迟、cache 命中降低,额外开销切换等方面因素。
Ainokiseki
11 小时 8 分钟前
@AnroZ 是这样,开三个后台线程就是说这个进程一共就四个线程,只有 step1 和 step2 的时候会往队列里放任务,然后等任务做完之后才会继续下一个 step ,所以 step0 的时候这三个后台线程应该是在等条件变量,什么都不会做。所以 step0 和单线程的时候应该是一模一样的。
@linzyjx step0 做的事情比较杂,就是一些赋值,一些 if 语句什么的,没有什么特点,没有计算密集的任务,也没有 io 密集的任务,都是在内存里倒来倒去。这是一个模拟芯片的程序,step0 在模拟芯片内信号的传递。
darklinden
11 小时 2 分钟前
如果你 step0 为了多线程使用做了大内存数据的复制,而单线程时只读/无冲突所以用了同一数据,可能 io 耗时超过计算减少的耗时的吧
JoeJoeJoe
11 小时 0 分钟前
感觉 op 这个不太适合这么拆分优化.

如果有前后依赖关系的话, 感觉应该把 step0-3 打包起来放在一个线程里面执行, 再来任务的话就再起一个线程执行 step0-3.
MrVito
10 小时 45 分钟前
你试下绑核吧,说不定有奇效
AnroZ
10 小时 37 分钟前
整体看还行吧,原来执行要花 17.5s ,现在 14s 。
如果你的 step 里涉及到 io 操作的话,测试时间本来就有一定的随机性。
建议,测 100+次,会更好一些
RinGress
10 小时 27 分钟前
@MrVito 前几年干过类似的活(但是是图像处理相关的,一张图数据量大,算法有并行优化空间),开 o3 、绑核之类操作加上去确实提升很多(但单线程在这些操作下可能也有一定提升,只是多线程提升可能大一点),再细就要细细看编译器生成的汇编去优化了。
@AnroZ 这种测量确实需要定量反复多次去做。
Building
10 小时 22 分钟前
这个要看多线程任务和访问的数据是不是互相独立的,否则访问时会产生多次拷贝和加锁,相比单线程会更加耗时
sjkdsfkkfd
9 小时 12 分钟前
需要具体看 step1,2,3 在干啥。通用的来讲,你可以使用自顶向下的微架构分析方法来一层层看问题在什么地方,推荐阅读 《现代 CPU 性能分析与优化》 这本书
jones2000
8 小时 30 分钟前
设计有问题。 上锁了, 避免用锁。锁很耗时间的。
minami
8 小时 25 分钟前
不了解你的各个 step 怎么拆分的,但是如果你需要主线程等待所有消费者线程运行结束的话,建议用 openmp/tbb ,别自己写
Ainokiseki
8 小时 20 分钟前
@minami 我等待子线程是 enqueue 之后返回一个 future ,之后调用 join ,总之是用的 c++多线程库

@JoeJoeJoe 我也想,task 粒度肯定越大越好,但是每个 step 之后都需要同步一下,要不然我担心出问题

@MrVito 我试了试,绑了核心之后 context switch 不降反增。。搞不懂为什么。我已经按 ai 的建议排除掉了 interrupt 最多的那些核心,但还是不行
minami
8 小时 15 分钟前
@Ainokiseki #13 不能这么写,你要用事件等。首先你每个子线程间不能有锁,其次你要给每个子线程注册一个完成的事件,然后在主线程用 WaitForMultipleObjects 之类的机制去等所有事件完成
ivan_wl
8 小时 14 分钟前
后台线程和主线程是不同的 cpu 吗
chashao
8 小时 1 分钟前
@sjkdsfkkfd 感谢分享这本书
MrVito
7 小时 56 分钟前
@Ainokiseki task 做完以后是 notify_one 还是 notify_all ?有可能惊群了
unused
7 小时 48 分钟前
看看是不是 NUMA 的问题,profiling 可以再细一点
bluearc
5 小时 47 分钟前
@Ainokiseki #3 意思是每次回到 step0 后,线程池中就没有任务了吗?那我觉得这里还可以优化一下(但看描述似乎下一个 step0 必须在上一个 step 执行完成后才能启动?),开一个大的线程池,把 step0 包装一下也扔到线程池里,step 执行完后启动 task2 ,3 的时候传一个回调函数进去,同时设计一个数据结构至少包含一个 atomic<size_t>来同步状态(如果能用 C++20 那可以用 latch ),比如初值为 2 ,task2 ,3 执行完成后就 fetchsub ,再检查值是否为零,如果是 0 就把回调扔到线程池里。
但 step 单线程和多线程下用时增长,倒也没什么思路,不如换个机器测测,或者像上面说的构造多个不同的数据多测几百次统计平均值

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/1172208

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX