并行实现树遍历算法的策略?

时间:2010-02-09 00:12:18

标签: design-patterns tree parallel-processing algorithm tree-traversal

我已经实现了迭代算法,其中每次迭代都涉及预订树遍历(有时称为向下累积),然后是后序树遍历(向上累积)。对每个节点的每次访问涉及计算和存储要用于下次访问的信息(在随后的后序遍历或后续迭代中)。

在预订序遍历期间,只要处理了它与根之间的所有节点,就可以独立处理每个节点。处理完成后,每个节点都需要将元组(特别是两个浮点数)传递给每个子节点。在后序遍历中,只要已经处理了所有节点的子树(如果有的话),就可以独立处理每个节点。处理完成后,每个节点都需要将一个浮点数传递给它的父节点。

在算法期间,树的结构是静态的并且不变。但是,在向下遍历的过程中,如果传递的两个浮点数都变为零,则不需要处理该节点下的整个子树,并且可以开始该节点的向上遍历。 (必须保留子树,因为后续迭代中传递的浮点数在此节点处可能变为非零,并且将继续遍历)。

每个节点的计算强度在整个树中是相同的。每个节点的计算都是微不足道的:只需要几个总和,然后乘以/除以长度等于节点上子节点数的数字列表。

正在处理的树是不平衡的:典型节点将有2个叶子加上0-6个额外的子节点。因此,简单地将树划分为一组相对平衡的子树是不明显的(对我而言)。此外,树被设计为消耗所有可用的RAM:我可以处理的树越大越好。

我的串行实现仅在我的小测试树上达到每秒1000次迭代的顺序;对于“真正的”树,我预计它可能会减慢一个数量级(或更多?)。鉴于该算法需要至少1亿次迭代(可能高达10亿次)才能达到可接受的结果,我想并行化算法以利用多个核心。我对并行编程没有经验。

鉴于算法的性质,推荐的并行化模式是什么?

3 个答案:

答案 0 :(得分:3)

尝试将您的算法重写为由pure functions组成。这意味着每一段代码本质上都是一个(小)静态函数,不依赖于全局变量或静态变量,并且所有数据都被视为不可变 - 只对副本进行更改---并且所有函数只能操作通过返回(新)数据来表达(在松散的“操纵”一词中)。

如果每个函数都是referentially transparent ---它只依赖于它的输入(并且没有隐藏状态)来计算它的输出,并且每个具有相同输入的函数调用总是产生相同的输出---那么你并行化算法的好处:因为你的代码永远不会改变全局变量(或文件,服务器等),所以函数所做的工作可以安全地重复(重新计算函数的结果)或者完全忽略(以后的代码不依赖)关于这个函数的副作用,所以完全跳过一个调用不会破坏任何东西)。然后当你运行你的函数套件时(例如在MapReducehadoop等的某些实现上),函数链将仅仅基于一个函数的输出而产生神奇的级联依赖关系。另一个函数的输入,以及您尝试计算的内容(通过纯函数)将与您尝试计算它的ORDER完全分开(一个问题通过MapReduce等框架的调度程序的实现来回答)。

学习这种思维方式的好地方是用编程语言Haskell(或者F#或Ocaml)编写算法,它对并行/多核编程有很好的支持,开箱即用。 Haskell强制你的代码是纯粹的,所以如果你的算法有效,它可能很容易并行化。

答案 1 :(得分:2)

通常的方法是使用某种深度优先的工作分裂。您可以从一些等待空闲队列的工作程序开始,一个工作程序在根目录开始遍历。具有工作的工作人员首先遍历深度,并且每当它处于具有多于一个子节点的节点时,它检查空闲工作队列,并且如果不是空的,则将子树(子)转移到另一个工作者。当一个工人完成一个子树时,有一些复杂处理加入,但一般情况下,这可以很好地适用于各种树结构(平衡或不平衡)

答案 2 :(得分:2)

这个答案描述了我如何使用我日常工作的并行语言和运行时系统Charm++。请注意,此框架中用于顺序代码的语言是C或C ++,因此您必须付出一些努力来移植计算代码。 Charm ++确实有一些与Python代码互操作的机制,尽管我对这些方面不太熟悉。您可以将驱动程序和接口代码保存在Python中,并将繁重的计算代码放在C ++中。无论如何,对顺序代码的移植工作可能会为您带来良好的下一次性能提升。

<强>设计

创建一个并行对象数组(在我们的环境中称为 chares ),并为每个内部树节点分配工作列表,从一些子树根开始并向下延伸。附加到这些节点的任何叶子也属于该角色。

每个并行对象都需要两个异步远程可调用方法,称为入口方法passDown(float a, float b)passUp(int nodeID, float f),这些方法将是它们之间的通信点。 passDown将调用用于执行预订计算的任何节点方法,而具有对象外子节点的节点将在这些后代对象上调用passDown

完成所有向下工作后,对象将计算其叶子的向上工作并等待其后代。 passUp的调用将尽可能地计算接收对象的树,直到它遇到未从其所有子节点接收数据的父节点。当对象的根节点完成向上工作时,它将在持有父节点的对象上调用passUp。当完成整个树的根时,您就知道迭代已经完成。

运行时结果:

实现此功能后,运行时系统将为您处理并行执行。它将在处理器之间分配对象,甚至在不同的计算节点之间分配对象(因此大大提高了树的大小上限,因为可用内存可以扩展得更高)。跨处理器和节点的通信看起来就像进程内通信 - 异步方法调用。运行时可以对对象进行负载平衡,以使所有处理器尽可能在每次迭代中保持忙碌。

<强>调整:

如果你采用这种方式并调整并行性能,你还可以设置消息的优先级,以保持关键路径长度短。在我的头脑中,我建议的优先顺序将按此顺序工作

  1. 向下工作非零
    • 离根越来越近
  2. 向上工作
    • 离叶子更近了
  3. Charm ++与称为Projections的性能分析工具配合使用,可以进一步了解您的程序的执行情况。