在更精细的递归粒度中分割递归

时间:2012-01-09 19:52:40

标签: multithreading recursion parallel-processing

我考虑过将递归拆分成更小的递归大小,然后想知道它是否具有任何实际用途,同时也考虑并行性。

要明确我的意思,一个小例子(合并排序):

而不是:

...
merge_sort(b, m);
merge_sort(m, e);
merge(b, m, e);  
...

做这样的事情:

...
merge_sort_quad(b, m1);
merge_sort_quad(m1 + 1, m2);
merge_sort_quad(m2 + 1, m3);
merge_sort_quad(m3 + 1, e);
merge_quad(b, m1, m2, m3, e);
...

考虑到一个平行的例子,我认为这两个方面都没有基本的区别,因为它们可能会产生相同的结果:

void foo (..) {
    ...
    //using tbb::prallel_invoke() to call functions in parallel
    tbb::parallel_invoke(foo(..), foo(..)); 
    ...
}

void foo_parallel (..) {
    ...
    tbb::parallel_invoke(foo(..), foo(..), foo(..), foo(..));
    ...
}

我希望你们能解释一下,如果这完全是无用的和坏的,或者它是否依赖于算法,可能会有一些实际应用。我对此表示怀疑,因为它看起来有点像手动循环展开。

2 个答案:

答案 0 :(得分:2)

你是对的,确实这是通过merge-sort完成的。在你的问题中有一些不同的想法,有些还有其他含义,所以让我们把它们分开。我会回顾一些我认为你可能比我更清楚的事情,因为它会为阅读它的其他人提供一个更连贯的答案。

第一次递归。有一个逻辑递归,我们将问题分解为自身的重复版本,直到它们达到某些程度,它们是微不足道的(通常,通过将当前数字乘以一个较小的阶乘直到我们达到1来进行阶乘),并且我们有函数递归通过函数调用本身来模拟它。

逻辑递归是一种解决问题的技术。函数递归是一种反映它的编程技术。但是,函数递归的成本可能高于迭代等价物。因此,我们经常要么让我们的编译器将它们转换为迭代等价物,进行尾调用优化,这也是一样(通过去除递归调用的大部分或全部成本),或者当失败时,自己转换为迭代版本。 / p>

现在,在我们使用合并排序的特定类型的递归中,当我们解决问题时,我们增加了更简单的任务的数量。这不是n!成为n × (n - 1)!的单个任务,merge-sort成为合并序列的两半合并的两个任务,然后是合并结果的任务。

你已经做出了正确的跳跃,得出的结论是,这可能导致并行方法。还有一些其他功能让它变得有趣。如果我们将它分解为4次合并,就像你已经完成的那样,并将每次合并分配给不同的核心,那么每个核心将处理将紧密连接在一起的内存并将数据加载到缓存中(数据靠近在一起的方式可以帮助但是,一个线程写入另一个线程感兴趣的同一缓存行中的数据相对不太可能,并迫使它遭受缓存失效(“假共享”数据靠近在一起会伤害我们的方式) )。

这种排序很可能仅限于CPU和内存,如果超线程,每个核心1个线程或每个虚拟处理器最多1个线程可能没什么好处。

因此,拆分为单独的函数调用可以获得高达虚拟处理器数量的性能。您问题中的示例将是四处理器计算机上的想法。在那之后,当一个线程结束时,一个线程不太可能通过工作窃取来帮助太多,所以从那时起你可能更好地采用迭代方法(无论是手动编码还是转向进入这样的编译器)。采用功能递归的方法超出了我们每个处理器的功能,这开始再次伤害我们。但是,我们总是可能错误地计算了我们实际需要使用的核心数(因为其他进程也在使用它们),因此它可能比每核心函数更进一步,并允许那些先完成的核心左撇子。

文献中并行合并排序有很多东西,有些框架和库有合并排序实现,可以利用它。

答案 1 :(得分:1)

我怀疑任何从后一种方法中获得任何性能优势的语言或机器都会从完全放弃的递归中获得更大的一种语言或机器。所以这并不会让我觉得有用;似乎只会引入不必要的复杂性。

但总有一个特例。甚至可能有一个我根本不知道的共同点。