代码之家  ›  专栏  ›  技术社区  ›  Anne Quinn

如何提高将数据推送到互斥锁队列的性能

  •  3
  • Anne Quinn  · 技术社区  · 6 年前

    我有一个“jobs”队列(函数指针和数据)从主线程推送到它上面,然后通知工作线程弹出数据并运行它。

    功能非常基本,如下所示:

    class JobQueue {
    public: 
        // usually called by main thread but other threads can use this too
        void push(Job job) {
            {
                std::lock_guard<std::mutex> lock(mutex);   // this takes 40% of the thread's time (when NOT sync'ing)
                ready = true;
                queue.emplace_back(job);
            }
            cv.notify_one();   // this also takes another 40% of the thread's time
        }
    
        // only called by worker threads
        Job pop() {
            std::unique_lock<std::mutex> lock(mutex);
            cv.wait(lock, [&]{return ready;});
            Job job = list.front();
            list.pop_front();
            return job;
        }
    
    private:
        std::list<Job>            queue;
        std::mutex                mutex;
        std::condition_variable   cv;
        bool                      ready;
    };
    

    但我有个大问题, push() 真的很慢 . 工作线程超过了主线程,在我的测试中,添加作业是主线程所做的全部工作。(工作线程执行20个4x4矩阵旋转,这些矩阵旋转互相馈送并在最后打印,因此它们不会被优化掉),随着可用工作线程的数量增加,情况似乎变得更糟。如果每个“作业”都更大,比如说100个矩阵运算,那么这个负数就消失了,更多的线程==更好,但实际上我给它的作业要小得多。

    最热门的调用是互斥锁和 notify_one() 这占了40%的时间,其他的一切似乎都微不足道。另外,互斥锁很少等待,它几乎总是可用的。

    我不确定我应该在这里做什么,是否有一个明显的或不那么明显的优化,我能做到这将有帮助,或者我可能犯了一个错误?任何见解都将受到极大的赞赏。

    (这里有一些我采用的度量标准,如果可能有帮助的话,它们不计算创建线程所需的时间,即使是数十亿个作业,模式也是一样的)

    Time to calc 2000000 matrice rotations
    (20 rotations x 100000 jobs)
    threads   0:       149 ms  << no-bool baseline
    threads   1:       151 ms  << single threaded w/pool
    threads   2:        89 ms
    threads   3:       120 ms
    threads   4:       216 ms
    threads   8:       269 ms
    threads  12:       311 ms  << hardware hint
    threads  16:       329 ms
    threads  24:       332 ms
    threads  96:       336 ms
    

    enter image description here

    enter image description here (所有工作线程都有相同的模式,绿色表示执行,红色表示等待同步)

    2 回复  |  直到 6 年前
        1
  •  2
  •   davidbak    6 年前

    医生:在每项任务中做更多的工作。(可能每次从队列中取出多个当前任务,但还有许多其他可能性。)

    你的任务(计算上)太小了。一个4x4矩阵乘法只是几个乘法和加法。约60-70次行动。其中20个一起完成并不贵多少,~1500(流水线)算术运算。线程切换的成本(包括唤醒等待cv的线程,然后唤醒实际上下文切换)可能高于此(可能) 许多的 较高的。

    此外,同步(互斥操作和cv)的成本非常昂贵,尤其是在争用的情况下,尤其是在硬件本机同步操作比算术(由于多核之间的缓存一致性强制)昂贵得多的多核系统上。

    这就是为什么你观察到,当每项任务都进行100次矩阵运算时,问题会减少,从20次增加到了20次:工人们回到井里,为了更多的事情做得太频繁,导致了争论,当他们只有20个MMS要做的时候……给他们100个任务,使他们的速度减慢到足以减少竞争。

    (在注释中,您指出只有一个供应商,几乎消除了作为队列争用源的问题。但是,即使在那里,在cv锁下排队的任务也比排队的任务多,这就更好了——达到了限制工人接受任务的极限。)

        2
  •  1
  •   ravenspoint    6 年前

    我建议使用事件处理程序。

    事件有两种类型:

    • 新工作到达
    • 工人完成工作

    主线程维护一个作业队列,仅由主线程访问(因此没有互斥锁)

    当一个作业到达时,它被放置在作业队列中。 当一个工人完成一项工作时,一项工作就会弹出并传递给该工人。

    在启动和没有可用作业时,您还需要一个空闲的工作队列。

    您还需要一个事件处理程序。这些都是棘手的,所以最好使用经过良好测试的库,而不是滚动您自己的库。我使用boost::asio