【问题标题】:How to improve performance of pushing data to a mutex locked queue如何提高将数据推送到互斥锁队列的性能
【发布时间】:2019-01-01 06:58:34
【问题描述】:

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

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

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

(所有工作线程都有相同的模式,绿色是执行,红色是等待同步)

【问题讨论】:

  • 批量作业。与其一次添加一份工作,不如一次添加一大堆。使用每个工作者的作业队列,并让生成每个作业的主线程将其添加到每个工作者的作业队列中。还有许多其他可能的变化,这完全取决于个人情况。
  • 条件变量在存在争用时具有显着性。当你抓住pop()中的锁时,不要wait如果ready为真
  • @Chad - 哦,我以为它在尝试等待之前检查了谓词。我尝试围绕它添加另一个检查,但不幸的是没有任何改进。
  • @SamVarshavchik - 我将尝试添加每个工作人员的作业队列。我最初避免使用它,因为它使加入变得更加困难,但在这种情况下它可能是值得的。批处理也
  • 1) 有什么理由必须扩展到 96 个线程?为什么不使用具有与可用内核相同数量的线程的线程池? 2)您希望工作需要多少毫秒?如果作业很短,最好进入无锁队列,而不是使用重量级的互斥锁/cv 同步。

标签: c++ multithreading optimization


【解决方案1】:

TL;DR:在每项任务中做更多的工作。 (也许每次从队列中取出一个以上的当前任务,但还有很多其他的可能性。)

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

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

这就是为什么当每个任务执行 100 次矩阵运算(从 20 次增加到20 个 MM 要做...给他们 100 个做会减慢他们的速度,从而减少争用。

(在评论中,您指出只有一个供应商,几乎消除了作为队列争用的来源。但即使在那里,在 cv 锁定下可以一起排队的任务越多越好 - 向上达到阻止工人执行任务的极限。)

【讨论】:

    【解决方案2】:

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

    事件有两种类型:

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-08-12
      • 1970-01-01
      • 2015-05-25
      • 1970-01-01
      • 1970-01-01
      • 2018-05-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多