Linus Torvalds:忘掉那该死的并行吧!

phpopen 10年前

在 Avoiding ping pong 上,Linus Torvalds 以其一贯高雅的调调抨击了“并行计算就是未来”的论调,并在原文和 Reddit 上收获了数百条评论。虽然事情最终也没有一个结果,但是许多观点确实值得借鉴。

Linus 论点如下:

推崇并行只不过是浪费大家的时间,“并行更高效”这种理论纯属胡说八道。大容量缓存是高效的,如果缺少缓存,并行一些低等级微内核可以说是毫无意义,除下特定类型上大规模规则计算,比如图形处理。

没有人会回到过去,那些复杂的乱序运行内核不会消失。扩展不可能无休止的进行,人们需求更多的移动性,那些叫嚣扩展到上千核心的论调纯属扯淡,无需理会。

是有多么奇葩的思维才能幻想出这些神奇等等并行算法的用武之地?!

对于并行来说,唯一的用武之地就是图形计算和服务器端,而并行计算在这些领域确实也得到了大量的应用。但是没有任何疑问,并行在其他领域毫无用武之地。

所以,忘掉并行吧,它永远都不可能被大规模推广。对于终端用户来说,4 核就差不多了,而在这个领域,如果不增加太多的能耗,你也无法塞入更多的内核。同时,也不会有智障去阉割内核,降低其大小和性能只为了多塞几个。通常情况 下,阉割内核只是为了降低功耗,因此这里也不会有那么多阉割的内核让你使用。

因此,讲究程序的并行性本质上就是错的,它基于了一个错误的前提,同时也只是一个早该过时的时髦术语。

在图形计算和服务器端之外,并行并不是万金油。即使在未来全新的领域同样如此,因为你根本承担不起。如果你期望做低功耗计算机视觉,我敢肯定你一定 不会在 GP CPU 上编码。你甚至不会去使用 GPU,因为它的开销太大了。大部分情况下,你可能会选择一些特殊的硬件——可能会基于某些神经网络模型。

放弃吧。“并行就是未来”的说法纯属胡说八道。

在看讨论之前,我们首先看一下 Linus 以 reference counting 为例说明了并行的复杂性(该部分转自 CoolShell

在 Linus 回复之前有人指出对象需要锁机制的情况下,引用计数的原子性问题: 

由于(对象)通过多线程方式及多种获取渠道,一般而言它需要自身维护一个互斥锁——否则引用计数就不要求是原子的,一个更高层次的对象锁足矣。

而 Linus 不那么认为: 

引用计数的问题在于你经常需要在对象数据上锁保护之前完成它。

问题有两种情况,它们锁机制是完全不一样的:

  • object *reference* 对象引用
  • object data 对象数据

对象数据保护一般是一个对象拥有一个锁,假设你没有海量扩展性问题,不然你需要一些外部大一点的锁(极端的例子,一个对象一个全局锁)。 

但对象引用主要关于对象的寻找(移除或释放),它是否在哈希链,一棵树或者链表上。当对象引用计数降为零,你要保护的不是对象数据,因为对象没有在其它地方使用,你要保护的是对象的寻找操作。 

而且查询操作的锁不可能在对象内部,因为根据定义,你还不知道这是什么对象,你在尝试寻找它。 

因此一般你要对查询操作上锁,而且引用计数相对那个锁来说是原子的(译者注:查询锁不是引用计数所在的对象所有,不能保护对象引用计数,后面会解释为何引用计数变更时其所在对象不能上锁)。 

当然这个锁是充分有效的,现在假设引用计数是非原子的,但你常常不仅仅使用一种方式来查询:你可能拥有其它对象的指针(这个指针又被其它对象的对象锁给保护起来),但同时还会有多个对象指向它(这就是为何你第一时间需要引用计数的理由)。 

看看会发生什么?查询不止存在一个锁保护。你可以想象走过一张对象流程图,其中对象存在指向其它对象的指针,每个指针暗含了一次对象引用,但当你走过这个流程图,你必须释放源对象的锁,而你进入新对象时又必须增加一次引用。 

而且为了避免死锁,你一般不能立即对新对象上锁——你必须释放源对象的锁,否则在一个复杂流程图里,你如何避免 ABBA 死锁(译者注:假设两个线程,一个是A->B,另一个B->;A,当线程一给A上锁,线程二给B上锁,此时两者谁也无法释放对方的锁)? 

原子引用计数修正了这一点,当你从对象A到对象B,你会这样做: 

  • 对象A增加一次引用计数,并上锁。
  • 对象A一旦上锁,A指向B的指针就是稳定的,于是你知道你引用了对象B。
  • 但你不能在对象A上锁期间给B上锁(ABBA 死锁)。
  • 对象B增加一次原子引用计数。
  • 现在你可以扔掉对象A的锁(退出对象A)。
  • 对象B的原子引用计数意味着即使给A解锁期间,B也不会失联,现在你可以给B上锁。

看见了吗?原子引用计数使这种情况成为可能。是的,你想尽一切办法避免这种代价,比如,你也许把对象写成严格顺序的,这样你可以从A到B,绝不会从B到A,如此就不存在 ABBA 死锁了,你也就可以在A上锁期间给B上锁了。 

但如果你无法做到这种强迫序列,如果你有多种方式接触一个对象(再一次强调,这是第一时间使用引用计数的理由),这样,原子引用计数就是简单又理智的答案。 

如果你认为原子引用计数是不必要的,这就大大说明你实际上不了解锁机制的复杂性。 

相信我,并发设计是困难的。所有关于“并行化如此容易”的理由都倾向于使用简单数组操作做例子,甚至不包含对象的分配和释放。 

那些认为未来是高度并行化的人一成不变地完全没有意识到并发设计是多么困难。他们只见过 Linpack,他们只见过并行技术中关于数组排序的一切精妙例子,他们只见过一切绝不算真正复杂的事物——对真正的用处经常是非常有限的。(译者注:当 然,我无意借大神之口把技术宗教化。实际上 Linus 又在另一篇帖子中综合了对并行的评价。) 

哦,我同意。我的例子还算简单,真正复杂的用例更糟糕。 

我严重不相信未来是并行的。有人认为你可以通过编译器,编程语言或者更好的程序员来解决问题,他们目前都是神志不清,没意识到这一点都不有趣。 

并行计算可以在简化的用例以及具备清晰的接口和模型上正常工作。你发现并行在服务器上独立查询里,在高性能计算(High- performance computing)里,在内核里,在数据库里。即使如此,人们还得花很大力气才能使它工作,并且还要明确限制他们的模型来尽更多义务(例如数据库要想做 得更好,数据库管理员得确保数据得到合理安排来迎合局限性)。 

当然,其它编程模型倒能派上用场,神经网络(neural networking)天生就是非常并行化的,你不需要更聪明的程序员为之写代码。 

在未来,应用程序究竟会发展成什么样?与现在有着非常大的区别?还是基本上相同,这里我们不妨看一下讨论(更多讨论见 RedditAvoiding ping pong):

Martin Thompson:

一旦工作集的大小超过了缓存容量,更大的缓存毫无意义。在低延时领域,为了保证整个应用程序放到缓存,我们通常会不择手段,但是这绝对不是主 流。使用更大的页,并让 L2 支持这些更大的页显然比实际缓存大小更有意义,当下我们已经可以看到很多大内存应用程序运行在 Haswell 上。

对比使用并行,通常情况下使用 cache friendly 或者 cache oblivious(实际上是 cache friendly 的升级)数据结构显然更具生产效率。时至今日,“如果把在 Fork-Join 和并行流上投入些许精力放到提供更好的通用数据结构上(比如 Maps 和 Trees,cache friendly)是否会更划算”这样辩论已经不再是困扰。在所谓的“多核问题解决”上,对比 FJ 和并行流,主流应用程序显然可以获得更多的提升。在这里,并不是说 FJ 和并行流不是个好的解决方案,而是后者可以给投资带来更多的回报。

在并行和并发上也有很多实际的用例,其中 Servlet 模型就是一个很好的例子,甚至是 PHP 之类在服务器端上的扩展。当然,在这之上,管道的构建也是一个更为直观的模型。

当谈到数据结构上的并发存取时,数据结构可变需要被单独对待。如果数据结构是不可变的,或者支持无阻塞并发读取,那么在并行上将很容易扩展,也 很容易被推断。并发修改任何有趣的远程数据结构(更不用说完整模型),管理起来都是非常复杂和困难的。抛开复杂性,任何从多个写入者到 1 个共享模型/状态的并发更新都会存在限制,这点已经被 Universal Scalability Law 证明。在核心越来越多的情况下,在需要扩展的情况下,我们经常和自己开玩笑——多个写入者到任何模型的更新是否是一个好主意。庆幸的是,在大多数开发的应 用程序代码中,查询针对的模型通常都不会有变化。

基于产业并发存取共享状态的需求,一个严重的后果产生:我们通常都会同步的进行这个过程,并在一个分布式的环境中传播。在算法和方法设计时,我 们需要拥抱异步方式以避免延时限制。通过异步方式,我们可以实现无阻塞访问,而基于强制隔离,我们可以让应用程序更好地执行,并拥有更好的弹性。带宽以高 速提升,延时将趋于平稳。

新一年我对平台提供商的愿望清单是:基础设施将有更好的 cache friendly 和 immutable,同时还具备让异步编程更容易的 Append-Only 数据结构、更好的管道并发、无阻塞 APIS(比如 JDBC)、语言外延(比如支持 state machines 和 continuations),以及可以做申明式查询的语言外延(比如 LINQ[3] for C#就可以提升)。同时也不要介意允许 Java 那种低等级访问,我们已经远超越了在浏览器沙箱中写程序的时代。

AntiProtonBoy:

通常情况下,我对 Linus 是非常不感冒的,但是公平来讲,这次他说的确实很有道理。大量核心一般是用在大规模分布式应用系统中,比如说你想模仿一个神经网络。而在这个情况,你肯定 也不会使用 20 万台个人电脑。他只是说在用户空间,30 个小的核心并不会比 4 到 8 个高速核心快,因此并行化在这里并没有什么优势,也只有在遭遇瓶颈时才考虑到瓶颈。

Gabriele Svelto:

着眼当下移动领域,“足够快”很可能并非优化的终点。现在大部分使用电池的计算设备都在致力让用户能够获得一个更快的感知速度,从而在总体上节 省电量。在这方面,某些并行算法完全处于劣势:在同等条件下,它们通过等价串行的方式,以增加计算(通常是通信)开销为代价来换取更快的速度。在实践中, 并不是所有计算之外的开销都是等价的,因此,你还需要分摊一些固定的开销,不过整体更快的执行可能更加有效;但也正是这样,衡量是否要增加某个负载的速度 将需要考虑更多变数。

Jeft:

Linus 的说法可以说对,也可以说不对。事实上,人们期待可以更有效利用并行硬件的途径已经相当久了,所以不能把这个作为新事物来看。事实上我们需要的不仅是语 言,如果你给它分配了太多工作,将从根本上挑战语言的基本结构,我们需求的语言是在需要的情况下可以最简单地并行,我们才刚刚开始。

Patrick Chase:

所有的一切都决定于容量和速度上的改变。当容量不足和(或者)算法集不稳定时,你使用的是商业硬件,而这十年的风格一直是 GPU。当容量变高了,算法更稳定了,你开始考虑定制硬件(当下,一般复杂的 ASIC 定制大约是 1000 万美元或者更高;结果就是,你可以通过数学来发现哪个更有意义)。

如果只是容量变高了,算法还有一部分不稳定,那么定制一个包含了固定功能硬件和可编程硬件(DSPs、GPUs 等等)的 ASIC 则非常有意义。这也是为什么高通公司为所有的 Snapdragons 都添加了“Hexagon”。

Maynard Handley:

当下,我们甚至没有开始程序员的再教育,让他们可以用更好的方式做事(更好的意思是抽象更匹配并行编程)。我们的语言、API 以及工具仍然很糟糕,就像使用 Fortran 来做递归和指针一样。当下我们的工具并没有重构,这也让我们避免去关心某个函数调用链是否增加了一个新的参数等。

Patrick Chase:

针对 Gabriele 提出的“那些问题可以通过选择不同的语言解决”,你说的对,但是在现实世界中根本不可行。对比 10 年前,并行技术在难易度和开销上并没有什么根本上的突破。没有出现神奇的编译器,没有出现突破性的方法和语言,Amdahl 法则并没有得到实质性的缓解。

序列化性能一定程度上受到了半导体工艺的限制,在这里我没看到任何微核心在 equilibrium 和 optimum 可以利用的因素。

因此,我觉得“anon”说的不错:并行方案只在必要的时候选用,提升单核性能则在任何可能的情况下。虽然这不是一个很好的愿景,但是却可以 work。

Linus Torvalds:

我可以想象到人们在服务器领域已经使用上了 60 核心,但是我们不认为这是件值得推广的事情。我认为,在服务器端增加更多的缓存和集成更多的 IO 同样更具效率。

在客户端方面,仍然存在一些类似工作站的负载可以使用 16 核心,我认为借助它们,美术家确实可以更快地做 Photoshop 和视频编辑工作。但是从全局来看,这部分市场份额非常之小,从台式电脑市场萎缩就可见一斑。

因此,市场的趋势更应该是“4 核心搭载大量的集成,既便宜又低功耗”。

但是,预测是困难的,特别是对未来,我们需要边走边看。

来自:http://news.cnblogs.com/n/512883/