V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Ainokiseki
V2EX  ›  C++

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

  •  
  •   Ainokiseki · 10 小时 10 分钟前 · 975 次点击
    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 多个空闲核心,开三个后台线程应该不会有什么影响。
    19 条回复    2025-11-12 16:41:05 +08:00
    AnroZ
        1
    AnroZ  
       9 小时 47 分钟前
    step0 干了什么?代码没看到,没办法帮忙分析。
    单独开个三个后台线程没什么问题,但每个人都这么想会有问题,应该有资源分配限制吧?
    RinGress
        2
    RinGress  
       9 小时 37 分钟前
    没代码不好说。
    能想到的是核间通信延迟、cache 命中降低,额外开销切换等方面因素。
    Ainokiseki
        3
    Ainokiseki  
    OP
       9 小时 32 分钟前
    @AnroZ 是这样,开三个后台线程就是说这个进程一共就四个线程,只有 step1 和 step2 的时候会往队列里放任务,然后等任务做完之后才会继续下一个 step ,所以 step0 的时候这三个后台线程应该是在等条件变量,什么都不会做。所以 step0 和单线程的时候应该是一模一样的。
    @linzyjx step0 做的事情比较杂,就是一些赋值,一些 if 语句什么的,没有什么特点,没有计算密集的任务,也没有 io 密集的任务,都是在内存里倒来倒去。这是一个模拟芯片的程序,step0 在模拟芯片内信号的传递。
    darklinden
        4
    darklinden  
       9 小时 26 分钟前
    如果你 step0 为了多线程使用做了大内存数据的复制,而单线程时只读/无冲突所以用了同一数据,可能 io 耗时超过计算减少的耗时的吧
    JoeJoeJoe
        5
    JoeJoeJoe  
    PRO
       9 小时 24 分钟前
    感觉 op 这个不太适合这么拆分优化.

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

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

    @MrVito 我试了试,绑了核心之后 context switch 不降反增。。搞不懂为什么。我已经按 ai 的建议排除掉了 interrupt 最多的那些核心,但还是不行
    minami
        14
    minami  
       6 小时 39 分钟前
    @Ainokiseki #13 不能这么写,你要用事件等。首先你每个子线程间不能有锁,其次你要给每个子线程注册一个完成的事件,然后在主线程用 WaitForMultipleObjects 之类的机制去等所有事件完成
    ivan_wl
        15
    ivan_wl  
       6 小时 38 分钟前
    后台线程和主线程是不同的 cpu 吗
    chashao
        16
    chashao  
       6 小时 25 分钟前
    @sjkdsfkkfd 感谢分享这本书
    MrVito
        17
    MrVito  
       6 小时 20 分钟前
    @Ainokiseki task 做完以后是 notify_one 还是 notify_all ?有可能惊群了
    unused
        18
    unused  
       6 小时 12 分钟前
    看看是不是 NUMA 的问题,profiling 可以再细一点
    bluearc
        19
    bluearc  
       4 小时 11 分钟前
    @Ainokiseki #3 意思是每次回到 step0 后,线程池中就没有任务了吗?那我觉得这里还可以优化一下(但看描述似乎下一个 step0 必须在上一个 step 执行完成后才能启动?),开一个大的线程池,把 step0 包装一下也扔到线程池里,step 执行完后启动 task2 ,3 的时候传一个回调函数进去,同时设计一个数据结构至少包含一个 atomic<size_t>来同步状态(如果能用 C++20 那可以用 latch ),比如初值为 2 ,task2 ,3 执行完成后就 fetchsub ,再检查值是否为零,如果是 0 就把回调扔到线程池里。
    但 step 单线程和多线程下用时增长,倒也没什么思路,不如换个机器测测,或者像上面说的构造多个不同的数据多测几百次统计平均值
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2965 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 12:52 · PVG 20:52 · LAX 04:52 · JFK 07:52
    ♥ Do have faith in what you're doing.