如何有效地并行化分而治之算法?

时间:2013-04-28 08:00:33

标签: c++ multithreading sorting c++11 parallel-processing

过去几天我对排序算法的记忆让人耳目一新,而且我遇到了一个无法找到最佳解决方案的情况。

我写了一个quicksort的基本实现,我想通过并行执行来提高性能。

我得到的是:

template <typename IteratorType>
void quicksort(IteratorType begin, IteratorType end)
{
  if (distance(begin, end) > 1)
  {
    const IteratorType pivot = partition(begin, end);

    if (distance(begin, end) > 10000)
    {
      thread t1([&begin, &pivot](){ quicksort(begin, pivot); });
      thread t2([&pivot, &end](){ quicksort(pivot + 1, end); });

      t1.join();
      t2.join();
    }
  }
}

虽然这比天真的“无线程”实现更好,但这有一个严重的局限性,即:

  • 如果要排序的数组太大或者递归太深,系统可能会用完线程并且执行失败。
  • 可能可以避免在每次递归调用中创建线程的成本,特别是考虑到线程不是无限资源。

我想使用线程池来避免后期线程创建,但我面临另一个问题:

  • 我创建的大多数线程最初都是完成所有工作,然后在等待完成时不做任何事情。这导致很多线程只是等待子调用完成,这看起来相当不理想。

我是否可以使用一种技术/实体来避免浪费线程(允许重用)?

我可以使用boost或任何C ++ 11工具。

3 个答案:

答案 0 :(得分:6)

  

如果要排序的数组太大或者递归太深,系统可能会用完线程并且执行失败。

所以在最大深度后顺序...

template <typename IteratorType>
void quicksort(IteratorType begin, IteratorType end, int depth = 0)
{
  if (distance(begin, end) > 1)
  {
    const IteratorType pivot = partition(begin, end);

    if (distance(begin, end) > 10000)
    {
      if (depth < 5) // <--- HERE
      { // PARALLEL
        thread t1([&begin, &pivot](){ quicksort(begin, pivot, depth+1); });
        thread t2([&pivot, &end](){ quicksort(pivot + 1, end, depth+1); });

        t1.join();
        t2.join();
      }
      else
      { // SEQUENTIAL
        quicksort(begin, pivot, depth+1);
        quicksort(pivot + 1, end, depth+1);
      }
    }
  }
}

使用depth < 5它将创建最多约50个线程,这将很容易使大多数多核CPU饱和 - 进一步的并行性将不会产生任何好处。

  

可能可以避免在每次递归调用中创建线程的成本,特别是考虑到线程不是无限资源。

睡眠线程的成本并不像人们想象的那么多,但是在每个分支上创建两个新线程没有意义,也可以重用当前线程,而不是让它睡觉......

template <typename IteratorType>
void quicksort(IteratorType begin, IteratorType end, int depth = 0)
{
  if (distance(begin, end) > 1)
  {
    const IteratorType pivot = partition(begin, end);

    if (distance(begin, end) > 10000)
    {
      if (depth < 5)
      {
        thread t1([&begin, &pivot](){ quicksort(begin, pivot, depth+1); });
        quicksort(pivot + 1, end, depth+1);   // <--- HERE

        t1.join();
      } else {
        quicksort(begin, pivot, depth+1);
        quicksort(pivot + 1, end, depth+1);
      }
    }
  }
}

除了使用depth之外,您可以设置全局线程限制,然后仅在尚未达到限制时创建新线程 - 如果有,则按顺序执行。这个线程限制可以是进程范围的,因此对quicksort的并行调用将从创建太多线程的合作中退出。

答案 1 :(得分:1)

我不是C ++线程专家,但是一旦你解决了线程问题,你就会有另外一个:

分区输入的调用未并行化。该调用非常昂贵(需要对数组进行连续迭代)。

您可以在维基百科中阅读qsort的并行部分:

http://en.wikipedia.org/wiki/Quicksort#Parallelization

它表明,以与您的方法大致相同的速度并行化qsort的简单解决方案是将数组划分为多个子数组(例如,与CPU核心数一样多),并行地对每个数组进行排序并使用合并结果合并排序的一种技术。

有更好的并行排序算法,但它们会变得相当复杂。

答案 2 :(得分:1)

直接使用线程来编写并行算法,特别是分而治之的算法是一个坏主意,你的扩展性很差,负载平衡很差,而且你知道线程创建的成本很高。线程池可以帮助后者而不是前者而不需要编写额外的代码。如今几乎所有现代并行框架都基于基于任务的工作窃取调度程序,例如Intel TBB,Microsoft并发运行时(Concert)/ PPL。

而不是产生线程或重新使用池中的线程,而不是“任务”(通常是一个闭包+一些簿记数据)被放到工作窃取队列中,以便在某个时刻运行X个工作线程数。通常,线程数等于系统上可用的硬件线程数,因此如果生成/排队数百/数千个任务(在某些情况下确实如此,但取决于上下文),这并不重要。对于嵌套/除法和conquer / fork-join并行算法,这是一个更好的情况。

对于(嵌套)数据并行算法,最好避免为每个元素生成一个任务,因为通常对单个元素的操作,工作的粒度太小而不能获得任何好处,并且由于调度程序管理的开销而等等。在较低级别的工作窃取计划程序的顶部,您有一个更高级别的管理,处理将容器分成块。这仍然是比使用线程/线程池更好的情况,因为你不再根据最佳线程数进行分割。

无论如何,在C ++ 11中没有这样的标准化,如果你想要一个纯粹的标准库解决方案而不添加第三方依赖项,你可以做的最好的是:

一个。尝试使用std :: async,像VC ++这样的一些实现将使用下面的工作窃取调度程序,但是没有保证,C ++标准也没有强制执行。

B中。在C ++ 11附带的标准线程原语的基础上编写自己的工作窃取调度程序,它是可行的,但不是那么简单,无法正确实现。

我认为只需使用英特尔TBB,它主要是跨平台的,并提供各种高级并行算法,如并行排序。