第 131 期·中文精读

克里斯·拉特纳:计算与编程语言的未来

Chris Lattner: The Future of Computing and Programming Languages

从 LLVM、Swift 到 MLIR,聊编译器设计、编程语言演进,以及硬件与软件协同的未来。

官方音频00:00 / --:--
中文精读由 AI 基于原始访谈改写,非官方翻译,可能存在偏差或错误;表述以原音频与官方文稿为准。说明

Chris Lattner 在 2000 年读大学时开始写 LLVM。这个项目后来变成了苹果、Google、以及几乎整个现代编译器世界脚下的地基。二十年后,他亲手搭起来的这套基础设施,在他眼里已经变成了需要被打破的对象。

他给出的理由很直接:LLVM 社区里有一批人已经在这个项目上工作了十年。十年是很长的时间。长到你开始看不见手里这件东西的毛病。他觉得这个社区正在走向僵化,需要新的项目进来搅局。他自己造的那个新东西叫 MLIR,现在已经挂在 LLVM 的伞下,但老一代 LLVM 开发者对它并不熟悉,于是"新的东西不可信"这种文化反应就出现了。

一个造了轮子的人,回头说轮子该换了。这期播客里他聊的东西远不止编译器:编程语言该怎么设计、指令集为什么要开放、摩尔定律到底死没死、并发模型该怎么写进语言、GPT-3 会不会改写编程、疫情之后硅谷会变成什么样。下面挑几条最有信息量的线索展开。

三个老板,三种带人的方式

访谈开场问的是 Steve Jobs、Elon Musk、Jeff Dean 各有什么过人之处。Lattner 跟 Elon 共事的时间比跟 Jobs 长得多,所以他做了个分组对比。

他的判断是:Jobs 更关注人的因素,Musk 更关注技术本身。

"Steve 想造的是让人喜欢、影响人生活的东西,他会往前面看一点,去看人们想要什么。Elon 更关注指数级增长的规律,去预测技术会怎么发展,而且他花大量时间和工程师泡在一起。"(Chris Lattner)

关于 Jobs 怎么让设计师跟工程师对话还赢得尊重,Lattner 的观察是,Jobs 推人推得很狠,但一旦对方给出了让他信服的解释,他就会放手。他对工程本身是尊重的,同时他又知道什么时候该继续压。读人读得准,你就知道对方是不是留了余力,还能不能再榨出一点东西。这是一种很难教的能力。

Jeff Dean 被归到完全不同的类别。Lattner 说他是个非常好的人,是典型的 Google 文化,希望身边的人都开心,同时又极其聪明,能把人聚起来。Jeff Dean 到今天还在写代码。他通过技术本身、通过自己动手去领导,然后把别人拉进来、激发他们。Lattner 说他甚至不觉得 Jeff 会想当 CEO 那种角色。

这三个人放在一起看,领导力没有统一答案。要看你面对的是什么时间、什么地点、什么问题。

不知道答案,不代表你没资格带队

Lattner 说他随着经验增长,越来越舒服的一件事是"接受自己不知道"。

"领导力很大一部分跟'你有正确答案'无关,跟'你能拿到正确答案'有关。"(Chris Lattner)

他讲了一个具体的操作:在团队里营造一种环境,别搞自上而下的"我让你做什么你就做什么",鼓励每个人都参与解决方案,并且提供一个安全的空间,让下属在你做错的时候敢开口告诉你。

他自己在船业公司待过一段时间,对船怎么设计几乎一无所知。结果发现只要肯问大量看起来很蠢的问题,变聪明的速度会非常快。前提是周围有一群愿意教、也愿意自己学的人。

这条经验贯穿了他整期播客。后来谈到疫情、谈到远程工作、谈到怎么学习新语言,他反复回到同一个点:把自己放进一个周围人都比你懂的环境里,然后问蠢问题。聪明人通常喜欢教别人,你只要准备好听,准备好成长,就能做出一些别人想不到的连接。

编程语言是思维的自行车

这是整期播客里最值得记下的一句。

"你可以走路,但你骑上自行车会快得多。编程语言就是思维的自行车。"(Chris Lattner)

他借的是 Jobs 那句"计算机是思维的自行车",但把它挪到了语言本身。逻辑是这样的:计算机擅长做特定的事,而人脑里有一个想法,想让它实现。从直接写汇编,到高级抽象,到设计一个神经网络让机器替你干活,中间有无数个层次可以停。停在哪里,就决定了你得到什么、付出什么。

这里有个值得记住的判断:编程语言有两条腿,一条是底层的机械部分,类型系统、编译器、怎么让计算机高效跑起来;另一条是 UI,也就是语言和人之间的界面。后面这条很多人不当回事,觉得无非是花括号、分号、空格还是 Tab,纯主观偏好。但 Lattner 坚持,UI 是一个设计问题,而且好的设计和坏的设计,人是能感觉出来的。

他的例子是 Swift。Swift 在底层有一堆好东西,同时在让代码"感觉良好"这件事上投入了巨大的人力。这不是玄学。

具体怎么让语言感觉良好?他给了一套很实用的判断标准。

  • 样板代码太多,你会觉得自己没在产出,这是坏事
  • 内存不安全,你会被悬空指针和各种 bug 缠住,调试到怀疑人生,这也是坏事
  • 把这些东西从体验里减掉,用户就会更开心

但这里有个陷阱。Stack Overflow 上那些高票答案,代表的是一小撮最活跃用户的优先级:安全的代码、规范的代码、稳定的代码。这不一定跟大多数普通用户的体验吻合。如果 99% 的人只关心"我想搭个 Web 服务器把事办了,多快能办成",那么语言设计者就要清楚自己到底在为谁优化。

所以语言设计真正要回答的是两个问题:有哪些库已经存在,以及你能多快把它们拼起来。语法层面的争论,在用户到达目标的过程中只占很小一块。

值语义:同时解决正确性和性能的那个甜点

这是技术含量最高的一段,值得慢慢拆。

Lattner 用 Tensor(张量)举了个例子。Tensor 就是一个多维数字网格,能做加减之类的运算。为什么在 Python 里你有时候必须显式 clone 一个 Tensor?

原因是 Python 用的是引用语义。你手里握着的是一个指针,它指向那个东西,你并没有直接持有它本身。你把 Tensor 传给一个函数,函数在里面加了点东西,你回到外面一看,发现它也变了。更麻烦的是,你把 Tensor 交给某个对象,它把引用存进了字典里,之后又往里塞数据。你以为你只交出去一份,对方以为自己是唯一的持有者,谁也不知道背后发生了什么。于是你只能 clone,因为你不确定自己是不是唯一持有者。

做机器学习、习惯用数学思维思考的人,会觉得这很反直觉。数学里对象是按值传递的,Python 却安静地不这么干。等你 debug 到一半,才发现要搞清楚克隆发生在栈的哪一层,还得先读懂那个库的实现。这就是纯粹的时间损耗。

Swift 用值语义解决了这个问题。

"在 Swift 里,Tensor 是一个值。你复制它,就看起来得到了一份独立的拷贝。你改其中一份,另一份不会跟着变,因为你只是复制了它。"(Chris Lattner)

听起来像每次操作都要付一次拷贝的代价。但 Swift 用了 copy-on-write:你传一个数组给函数,函数如果只是想读,大家共享同一份内存;只有当某一方真的要改,系统才真正复制。传出去的时候,如果只有一个引用,系统就知道可以就地更新,不用额外分配。

对比 Java。Java 的字符串是不可变的,所以每次拼接都要分配一个新的对象来装 x+y,因为别人可能还持有原来那个的指针。不可变性带来的安全,代价是性能。纯函数式语言在大量场景下都是这个模式。

Swift 的赌注是:把值语义做成默认,同时用 copy-on-write 拿回性能,可以得到一个其他语言里没见过的甜点区。

这个设计还有个更深的意图。它让数学在代码里真的像数学一样工作,同时又保留了程序员需要的底层效率。你不会为了正确性牺牲性能,也不会为了性能天天怀疑自己手里的是不是唯一引用。

这就是"渐进式披露复杂性"的一个实例。Swift 允许你从 print("hello, world") 开始,不必一上来就写 public static void main 加一堆访问控制、包、类。你正常写代码,默认是安全的、够快的。等你成了高级用户,想控制底层指针,可以去调 malloc,但没人会在第一页教程里教你这么干。这种分层的能力,是 Lattner 认为编程语言设计里最难也最有价值的部分。

语法糖之争与一场卸任

访谈里有一段关于 Python 3.8 引入 Walrus 操作符(海象操作符)的讨论,非常有意思,因为它不只是技术问题。

Lattner 先定义了什么算语法糖:把一个语言里已经存在的东西,用一种更简洁的方式写出来。他随即指出,这种特性最大的麻烦在于,它的价值几乎完全落在主观区间里:这件事发生的频率到底高不高,值不值得为它引入新语法?

他给了一个人类心理学的观察。面对一个新东西,人们会系统性地高估学习它的负担。它看起来很陌生,所以你抗拒。但如果它从一开始就在语言里,你就会说"这当然是 Python 的一部分,我熟得很"。

Walrus 操作符引发的争议最终导致 Guido van Rossum 卸任了 BDFL(终身仁慈独裁者)的位置。Lattner 对这件事的解读,视角比一般人深。

"我从中得到的感受是,他把决策者的重量压在了自己肩上,而且这件事和他人身份认同绑得太紧,所以他把它当成私人的事。他把自己放在了'那个人'的位置上,周围却没有一个支撑结构。"(Chris Lattner)

他的类比很妙:创始人知道公司是怎么一步步走到今天的,所以敢说"我们过去二十年都这么干,但今天我们换个活法",然后推动整个公司转型。但创始人走了之后,继任者往往不敢做这种决定,哪怕 CEO 理论上什么都能干。

原因有两个。

第一个是继任者没经历过那些决策的过程,不知道当初的决策依据是什么。一旦原则变了,你本该有义务改变做法、改变方向。但如果你不知道当初是怎么走到这一步的,那一切就像福音书,你不会去质疑它。

第二个更狠:即使你在理论上是大权在握的那个人,你也只是"自以为在掌权的人"。你得去激励团队,得解释、得让人理解、得让人真心认同。如果他们不信,方向就转不过来。领导者能行使的权力,是有上限的。

Swift 的做法是用一个核心团队来分摊这份重量,最终决策交给群体的智慧。这让决定不那么私人化,而且有公开的提案流程和决策理由记录。Lattner 把这种机制类比成最高法院的判例法:每次决定都有据可查,既能指导未来的讨论,又能充当一种防御机制。有人不满的时候,矛头指向的是"这个决定",跟"某个人做错了事"脱钩。

他也承认这个模式不完美。核心团队的每个人都是不同的人,观点不一样,还有 Apple、Google、SiFive 三个组织在里面。但正因为如此,从原则出发推导决定,再想办法让社区理解并认同,这条路才格外重要。

LLVM 的二十年之痒,和 MLIR 的野心

LLVM 到 2020 年 12 月就满二十岁了。Lattner 说这个项目正处在容易钙化的年纪。

他的原话里有一层很微妙的自省。他说自己的毛病是能看到 LLVM 的很多问题,这些问题一直硌着他,但他没时间去修。时间久了,它们像指甲缝里的沙子,你摆脱不掉。而别人看不到这些问题,也抗拒改变,于是"我们应该修这个"换来一句"听起来风险很大"。

他提出了一个新框架:MLIR(Multi-Level IR)。名字是编译器圈的人取的,取名水平堪忧,Lattner 自己都吐槽了。

MLIR 要解决的是什么问题?编译器历史上都是为某个特定领域造一整套工具。LLVM 擅长 CPU,JVM 擅长垃圾回收语言加动态编译,每个领域都从头造一遍,大量的东西重复实现。

MLIR 提供一套通用的编译器基础设施,让你能更快地为不同领域造出专用的编译器,而且结果质量还不错。它比 LLVM 更通用、更灵活,LLVM 反而是它的一个子集。目标领域包括自定义加速器、矩阵乘法硬件,以及一整个硬件综合的世界,也就是用来设计电路的编译器,编译的领域是晶体管。

这里有个关于社区文化的绝佳观察。MLIR 是由 Lattner 和一帮非常了解 LLVM 的人造的,所以他们修掉了 LLVM 里的一堆历史错误。但那些二十年来一直在维护 LLVM 经典部分的人,对新东西并不熟悉。新东西感觉上就是"不可信"的。

他没有为此愤愤不平。他觉得这种文化张力是健康的。思想会渗透,技术会扩散,人需要用自己的语言去重新理解一件事。这个过程很慢,但它在发生。

RISC-V:把指令集从公司手里拿出来

Lattner 现在在 SiFive 做平台工程的高级副总裁。这家公司由 RISC-V 的三位创始人建立,其中就有 Dave Patterson。

RISC-V 的核心特征是:它是一个开放标准。x86 只能由 Intel 和 AMD 造,ARM 的指令集只能由 ARM 公司授权出去,MIPS 归某家公司所有再授权。RISC-V 谁都能拿来造芯片。

这听起来抽象,但它解决的是一个非常现实的问题。过去几十年里,公司起起落落,指令集也跟着起落。Sun 的 SPARC 活到了今天,HP 的 PA-RISC 曾经承载了整个服务器业务和大量客户,后来 HP 决定转向 Intel 的 Itanium,结果并不好。

真正的问题在这里:你在一个由某家公司拥有的指令集上投入了巨额资金、大量工程和软件工作,围着它建了一堆产品,然后这家公司做了个技术决策或者商业决策,你就卡死了。哪怕强如 Intel,也不能保证每次都执行到位。

RISC-V 提供的是可选性。你从 SiFive 买一个实现,如果二十年后 SiFive 出了什么问题,没关系,你可以从别人那里买 RISC-V 核。整个生态里有人在造不同权衡的核。你有产品家族、有多种需求,大概率能在 RISC-V 空间里找到合适的东西。

Lattner 的展望是:未来会出现大量定制芯片。眼下的局面是,一家做智能硬件的公司如果没有 iPhone 那样的出货量,就负担不起定制芯片,只能买别人设计的、并不完全贴合自己需求的现成货,为用不上的硅付钱。如果设计芯片的成本降下来,定制芯片就会变多,电池续航会更好,方案会更贴合具体场景,上市速度会更快。

摩尔定律:三种解读,都没错

访谈里把 Dave Patterson、Jim Keller 和普通人对摩尔定律的理解摆在一起。

Jim Keller 说物理上还有上千倍的空间,晶体管还能更小更快。Lattner 认为他说得完全对,我们还没撞到物理极限。但那不是摩尔定律本来的意思。

摩尔定律的原始表述里有一个明确的经济维度:单位面积的成本,以及那个空间里最经济的点。Dave Patterson 常强调的正是这一点。按这个技术性的定义,摩尔定律确实变了。

Lattner 又给了第三种解读:单线程性能不再像以前那样提升了。以前你买一颗奔腾 66,前一年是奔腾 3,性能翻倍,而且是在做完全相同的事情时翻倍。同样的程序跑快一倍,你只要开张支票等一年半。很多人心里的摩尔定律就是这个,而这个已经死了。

取而代之的是什么?是逼着人换编程方式。你得写 CUDA 才能拿到 GPU 算力。你得习惯一台机器上有 50 个核、100 个线程。你有专为机器学习做的加速器。性能还在,但你得改编程模型。

而一旦谈到改编程模型,就绕回语言问题了。C 语言是为 CPU 设计的,想跟 GPU 说话,你得去用它的表亲 CUDA,那是一套不同的工具、不同的世界、不同的思维方式。现在还没有一个能统一缩放的世界,Lattner 觉得可以有一个,而且会好得多。

让并行成为语言的一等公民

聊到机器学习为什么能自动并行,Lattner 指出了一个漂亮的转折。

让一个 C 编译器或 Swift 编译器自动并行化命令式代码,还要推理所有指针,是非常难的问题。但换到机器学习的领域,那些做猫检测器的人,顺手搞出了一个极其强大的抽象层:一组可以拼装的算子。抽象层次被抬得足够高,自动并行化的编译器就突然可行了。你可以用 TensorFlow 写一个模型,让它在 1024 个 TPU 节点上跑。

"这套东西出自那些只想解决问题的人,加上社区的有机演化,编程语言圈的人并没有参与。"(Chris Lattner)

批量处理(batching)也是同一个道理。一个很朴素的想法,但威力巨大。

他自己写的《Swift 并发宣言》是另一种思路。写宣言的原因是,Swift 社区需要小而精细的提案,一次只推进一小步,但同时又需要大的方向。当你在做一件大事却要拆成小步走的时候,怎么知道不是在随机游走?宣言就是那个把大图景画出来、让后续的小步有参照的东西。

宣言里的核心思路有几层:

  • 多个计算机或多个线程通信时,最好是异步的。所以要能分别运行,再互相通信,这意味着需要建模异步通信的机制,比如 async/await
  • 往上一层是内存安全。两个线程同时碰同一块内存就是竞态条件,这种 bug 让人抓狂,因为通常无法复现。值语义在这里又派上用场,因为数学和拷贝在并发世界里同样成立
  • 再往上是给程序员的心智模型。宣言提出的方案是一个老模型的新版本:actor。逻辑上,你写的是一块单线程的孤岛,它跟其他东西异步通信
  • 继续往上,这些异步通信的东西不共享内存,那它们为什么必须在同一个进程里?可以跨进程,可以跨机器。于是你得到一条通往分布式编程的平滑路径
  • 再往上,加速器本质上是异步的东西,你通过发消息跟它通信。怎么给它们编程,这就很有意思了,而且不在提案范围内

这里有一个贯穿全篇的设计哲学问题:多少该是魔法,多少该交给人控制?默认情况错了的时候你怎么办?

Lattner 举了 Logo 作为反面例子。很好上手,专为教小孩设计,但你很快会撞到天花板,上不去了。只能换一个世界,重写所有代码。Python 的并发也有类似的困境:全局解释器锁让多线程很麻烦,你写了一定规模的程序之后,会发现自己被一系列糟糕的权衡困住。

他的偏好很明确:建一个能讲清楚的简单模型,同时留一个逃生舱。Swift 里默认你按标准写法写就不会踩坑,但如果你非要拿一个 C 级别的指针,可以显式去做。默认有护栏,护栏外有通道。

至于谁来决定代码的哪部分并行?在他的提案里是程序员。运行时和编译器要非常可预测。这跟历史上那批试图自动并行化的编译器形成对比。

那些编译器项目很有诱惑力:不改一行代码,换个编译器就快十倍,编译器会做模式匹配、深度分析,把结构体数组转成数组结构体之类的。麻烦的是,你加个功能、重构一下代码,突然性能掉了十倍。因为某个函数没被内联,你触发了编译器的某个启发式失效。

"作为用户,你不想知道,你不想知道编译器怎么工作、内存层次怎么工作、它怎么被并行化的。你希望这些被抽象掉。但一旦魔法失效,你就掉下悬崖,不知道该怎么办。"(Chris Lattner)

如果你在乎性能,可预测性就极其重要。

GPT-3 和 Software 2.0 的边界

Lattner 对 GPT-3 的反应分两面。一面是,把模型做大、把数据塞得更多,只要训练方法对、架构好,结果就更好,这在深度学习圈早就被理解透了,所以 GPT-3 本身不算意外。另一面是,让它真正跑起来需要海量的工程投入。

他提到 OpenAI 那篇引发讨论的博客:因为担心被滥用造成社会伤害,所以不发布。他认为这个担忧至今成立。用意良好的工具可以被用在很可怕的地方,影响可以非常深远。

关于 Andrej Karpathy 提出的 Software 2.0,也就是用学习模型替代越来越多的命令式代码,Lattner 的态度是:部分同意,但他把它看作一种新的编程范式,跟 Software 1.0 是并列关系,谈不上替代。

他给了一套很清晰的判断。机器学习解决某几类问题极其出色,而且是唯一正确的方式:

  • 你不会再用 C 代码去写一个猫检测器
  • 你不会再用 C 代码去写一个

本集资源与出处