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

时间:2018-12-31 20:37:30

标签: c++ multithreading optimization

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

这些功能非常基本,看起来像这样:

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矩阵旋转,这些旋转互相馈入并在最后打印,因此它们没有被优化掉)。这似乎也随着可用辅助线程数量的增加而恶化。如果每个“ Job”都较大,例如100个矩阵运算,则该负数消失,更多的线程==更好,但是我在实践中给出的Jobs远小于此数。

最热门的调用是互斥锁和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 个答案:

答案 0 :(得分:2)

TL; DR:在每个任务中做更多的工作。 (也许每次都将多个当前任务从队列中移出,但是还有许多其他可能性。)

您的任务(按计算)太小。 4x4矩阵乘法只是几个乘法和加法。 〜60-70次作业。将它们中的20个一起完成并不算昂贵,约有1500个(流水线)算术运算。线程切换的成本(包括唤醒等待在cv上的线程,然后唤醒实际的上下文切换)的成本可能比这更高-可能更高。

此外,同步(互斥量和cv的操作)的成本非常昂贵,尤其是在争用的情况下,尤其是在多核系统上,硬件本地同步操作比算术要贵得多(因为在多个内核之间实施了缓存一致性)。

这就是为什么您观察到,每个任务执行100个矩阵操作时,问题减少了,而原来是20个:工人们回到井里去做更多的事情,而他们经常这样做,导致争执20个MM可以做...给他们100个MM可以减慢它们的速度,从而减少争用。

(在注释中,您表明只有一个供应商,几乎消除了该供应商作为队列争用的源。但是即使在那儿,在cv锁定下可以一起排队的任务也越多越好-到最大程度阻止员工执行任务。)

答案 1 :(得分:1)

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

事件有两种类型:

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

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

当作业到达时,它将被放置在作业队列中。 工人完成工作后,会弹出工作并将其传递给工人

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

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