如何使用TBB并行化std :: partition

时间:2014-05-28 23:21:47

标签: c++ algorithm sorting parallel-processing tbb

有没有人有使用TBB有效并行化std :: partition的技巧?这已经完成了吗?

以下是我的想法:

  1. 如果数组很小,std :: partition it(serial)并返回
  2. else,使用自定义迭代器(在缓存大小的块中交错)将数组视为2个交错数组
  3. 为每对迭代器启动一个并行分区任务(递归到第1步)
  4. 交换两个分区/中间指针之间的元素*
  5. 返回合并的分区/中间指针
  6. *我希望在平均情况下,这个区域与数组的长度相比会很小,或者与在连续块中对数组进行分区时所需的交换进行比较。

    在我尝试之前有任何想法吗?

4 个答案:

答案 0 :(得分:3)

我将其视为并行样本排序的退化案例。 (可以找到样本排序的并行代码here。)设N是项目数。简并样本排序将需要Θ(N)临时空间,具有Θ(N)工作和Θ(P + lg N)跨度(关键路径)。最后两个值对分析很重要,因为加速仅限于工作/跨度。

我假设输入是随机访问序列。步骤是:

  1. 分配一个足够大的临时数组来保存输入序列的副本。
  2. 将输入分为K个块。 K是调整参数。对于具有P硬件线程的系统,K = max(4 * P,L)可能是好的,其中L是用于避免可笑小块的常量。 “4 * P”允许一些负载平衡。
  3. 将每个块移动到临时数组中的相应位置,并使用std :: partition对其进行分区。块可以并行处理。记住每个块的“中间”的偏移量。您可能需要考虑编写一个自定义例程,它都会移动(在C ++ 11意义上)并对块进行分区。
  4. 计算块的每个部分应该在最终结果中的偏移量。每个块的第一部分的偏移可以使用exclusive prefix sum在步骤3的中间偏移上完成。每个块的第二部分的偏移可以通过使用每个中间相对的偏移来类似地计算。到其块的 end 。后一种情况下的运行总和成为最终输出序列末尾的偏移量。除非您处理超过100个硬件线程,否则我建议使用串行独占扫描。
  5. 将每个块的两个部分从临时阵列移回原始序列中的适当位置。复制每个块可以并行完成。
  6. 有一种方法可以将步骤4的扫描嵌入到步骤3和5中,这样可以将跨度减小到Θ(lg N),但我怀疑它是否值得增加复杂性。

    如果使用tbb :: parallel_for循环来并行化步骤3和5,请考虑使用affinity_partitioner来帮助步骤5中的线程从步骤3中获取它们在缓存中留下的内容。

    请注意,对于Θ(N)内存加载和存储,分区仅需要Θ(N)工作。内存带宽很容易成为加速的限制资源。

答案 1 :(得分:2)

为什么不与std::partition_copy类似的东西并行?原因是:

  • 对于std::partition,就像Adam的解决方案中的就地交换需要对数复杂性,因为结果的递归合并。
  • 在使用线程和任务时,无论如何都会为并行性支付内存。
  • 如果对象很重,那么交换(共享)指针更合理
  • 如果结果可以同时存储,那么线程可以独立工作。

应用parallel_for(对于随机访问迭代器)或tbb::parallel_for_each(对于非随机访问迭代器)来开始处理输入范围非常简单。每个任务都可以独立存储'true'和'false'结果。有很多方法可以存储结果,有些方法来自我的头脑:

  • 使用tbb::parallel_reduce(仅用于随机访问迭代器),将结果本地存储到任务主体,并将其从join()移动附加到其他任务
  • 使用tbb::concurrent_vector的方法grow_by()复制本地结果,或者在到达时单独复制push()每个结果。
  • tbb::combinable TLS容器中缓存线程局部结果并稍后将它们组合

std::partition_copy的确切语义可以通过从上面临时存储中复制来实现,或者

  • (仅用于随机访问输出迭代器)使用atomic<size_t>游标来同步存储结果的位置(假设有足够的空间)

答案 2 :(得分:0)

您的方法应该是正确的,但为什么不遵循常规的分而治之(或parallel_for)方法?对于两个线程:

  1. 将数组分成两部分。把你的[开始,结束]变成[开始,中间],[中间,结束]。
  2. 并行地在两个范围上运行std :: partition。
  3. 合并分区结果。这可以通过parallel_for来完成。
  4. 这应该更好地利用缓存。

答案 3 :(得分:0)

  

在我看来,在我尝试之前,这应该很好地并行化,任何想法?

嗯......也许是几个:

  • 没有真正的理由创建比核心更多的任务。由于您的算法是递归的,因此您还需要在达到限制后跟踪不要创建其他线程,因为它只是一种不必要的努力。
  • 请注意,拆分和合并数组会降低处理能力,因此请以某种方式设置拆分大小,这实际上不会减慢计算速度。拆分10个元素阵列可能很诱人,但不会让你到达你想要的位置。由于std::partition的复杂性是线性的,因此很容易高估任务的速度。

既然你问过并给出了一个算法,我希望你真的需要在这里进行并行化。如果是这样 - 没有什么可以补充的,算法本身看起来很好:)