并行计算的解药

前几天看到 reddit.com 的 programming 类别第一名是《 Parallelism is Not Concurrency 》。读完之后发现和我去年的《多核与锁》有很多观点上的共通之处。《 Parallelism is Not Concurrency 》的开篇行文更流畅幽默,对并发( concurrency )和并行( parallelism )有更精辟的总结。比如:

Concurrency is concerned with nondeterministic composition of programs (or their components).  Parallelism is concerned with asymptotic efficiency of programs with deterministic behavior. Concurrency is all about managing the unmanageable. … Parallelism, on the other hand, is all about dependencies among the subcomputations of a deterministic computation.  The result is not in doubt, but there are many means of achieving it, some more efficient than others.

谈及『依赖( dependency )』在并行计算方面的关键地位之后,《 PINC 》进行了一个有趣的推理:

  1. 因为依赖是个关键概念而且它是高于硬件的抽象概念,所以我们需要一个基于语言的并行计算模型;
  2. 因为依赖阻碍并行,所以我们需要一个消除了依赖的并行计算模型;
  3. 由 1 和 2 ,我们需要一个能最大限度消除依赖的编程语言;
  4. 所以,functional 编程语言是解决并行计算的终极形态。

这个四步推导其实非常脆弱。首先看第 2 步,它的结论是需要一个『消除了依赖的并行计算模型』,这与《多核与锁》中谈到并行计算需要把数据分割成没有依赖关系的独立块的说法是一致的。但是,『消除依赖』是指在数据中消除依赖,是指对问题本身,或称为『问题域』消除依赖。因此准确的推导应该在第 3 步中提及的『最大限度消除依赖的编程语言』之前加上『在问题域中』这个修饰语。但是,很不幸,第 4 步的 functional 编程语言只是在编程阶段,或者称为『实现域』中消除了程序员引入依赖的能力。

人们经常想当然地把『消除』视为『消除烦恼』的同义词。但是,只有在问题域中的消除才基本保持这个同义性。而在实现域中编程语言消除程序员的某种能力更贴近于『强迫』。『强迫』的好处在于让程序员更自觉的用更好的更清晰的方法考虑问题。前提是,程序员已经拥有更好的更清晰的方法,只是经常限于时间和精力图省事抄近路采用一些丑陋的方法。这个前提在很多时候是成立的,比如说,程序员都能轻易的避免使用混乱的 goto ,而另一方面,他们又都倾向于随意使用 goto ,所以,一种禁止使用 goto 的编程语言是好的(当然,C 并没有禁用 goto ,而是把 if-elsewhilefor 等等分支功能做得易用而且优美,从而比粗暴的禁止更好发挥作用)。

很明显,这个前提在并行计算中不成立!程序员还没有一种真正通用的方法可以在问题域消除影响并行的依赖。假设一个程序员精通各种顺序排序算法,唯独没有学习过 Quick Sort 算法。那么即使学会 functional 编程语言之后,这个程序员也不可能自动的把原来的顺序算法改写成适合并行的形式。消除依赖需要在问题域的艰深研究而不是实现域编程语言的强迫。研究万有引力需要高深的微积分工具,而其结论万有引力公式只需要简单的代数知识就能在大多数场合应用。类似的,一旦在问题域中取得了消除依赖的突破,传统的编程语言往往也能很轻易的实现并行。例如并行编译这个问题,因为 C 语言在设计的时候就保证了单个文件在编译时没有对其它文件的依赖,所以并行编译只要简单的多运行几个编译器进程就能实现。(不要把这里的『设计语言』和原文四步推导中的『语言』混淆。对于编译来说,C 语言的设计是问题域而非实现域。)这时形式上消除依赖的 fucntional 编程语言倒是多余的了。

另一个例子是 3D 图形处理,这个问题包含大量的数据计算,比如顶点位置。而每个顶点的位置可以独立计算没有相互依赖。因此业界可以为这个问题建立消除依赖的硬件模型 —— GPU( 同样,这里的消除是『强迫解除能力』的意思,因为问题域本身天然地免除了依赖 )。这就给《 PINC 》中的第 1 步推导提出了反例,对天然免除了依赖的问题是可以建立基于硬件的模型的。而另一方面,因为 GPU『强迫解除』了通用 CPU 的很多能力 —— 比如要求数据的小粒度分割和缺乏对分支的支持,当业界想把 GPU 用于通用并行计算的时候,遇到的最头疼的问题是如何把原来在 CPU 上运行的程序改写成适合 GPU 的形式。当 functional 编程的拥趸苦恼 functional 语言的小众化现状的时候,其实已经有了一个流传广泛的低级 functional 编程语言,这就是 GPU 及其接口 OpenGL/OpenCL/CUDA 。这个模型已经在大多数 PC 上(包括 Mac )甚至很多 mobile device 上得到推广,实现了 functional 编程拥趸们在推广度方面的梦想。但是奇迹并没有自动出现,在问题域还是遗留了众多的工作要做。

所以,解决并行性能的关键,不在于急匆匆的在编程语言级别剥夺程序员引入依赖的能力,而在于研究更多问题的本质,去掉那些直观上存在但是并非本质上不可消除的依赖。实现域的『强迫解除』并不能带来问题的自动解决。等问题域的依赖被解除了,再考虑语言级别的问题不迟。即使到了那个时候,我也不认为 functional 语言会成为主流,因为和用来取代 gotoif-elseforwhile 相比,functional 语言解决问题的方式还是十分丑陋的。


你可能感兴趣的:(并行计算)