克里斯·拉特纳:编译器、LLVM、Swift、TPU 与机器学习加速器
Chris Lattner: Compilers, LLVM, Swift, TPU, and ML Accelerators
LLVM 与 Swift 之父回顾编译器设计历程,并谈 TPU 等机器学习加速器的工程取舍。
2019 年,你手里那部 iPhone 上的每一个 App,索尼用来渲染电影特效的机器集群,Google 生产服务器上跑的搜索服务,PlayStation 4 里的游戏,这些看起来毫无关系的东西,编译它们背后走的是同一套代码。
这套代码叫 LLVM。
它最初是伊利诺伊大学厄巴纳香槟分校的一个研究生项目,只有几个人在做。二十年后,它成了英特尔、AMD、英伟达、苹果、Google 这些平时在市场上往死里打的公司共同维护的公共地基。做这件事的人是克里斯·拉特纳(Chris Lattner)。他还顺手写了 Clang,那个今天几乎所有人都在间接使用的 C/C++/Objective-C 编译器,然后在苹果内部发起了 Swift。
这期 Lex Fridman 的访谈长达一小时十三分钟,信息密度极高。拉特纳几乎没讲什么励志故事,他讲的都是具体的:一个 for 循环怎么从语法树变成控制流图,寄存器分配为什么是编译器最硬的骨头之一,bfloat16 为什么把比特挪个位置就能让 TPU 快起来,以及他在特斯拉那五个月到底看到了什么。
一个只想读一年硕士的人,写出了二十年的基础设施
拉特纳的第一段程序是照着父母买的一本 Basic 教材敲进去的。他的回忆里没有什么顿悟时刻,只有一件事:敲进去,跑起来,敲错一个字符,然后盯着屏幕找原因。
“我开始的时候就是把书上的程序打出来,看看它们怎么工作,然后打错,再搞明白为什么跑不起来。”(克里斯·拉特纳)
这条路走得挺有意思。Basic 之后是 GW-Basic、QBasic、QuickBasic,都是微软系的,一层层往上叠。然后是 Pascal,然后是汇编,然后才是 C++。中间他还摸过 Smalltalk。Fridman 在节目里开玩笑说他走了黑魔法那条路,本来可以走 Lisp 那条更高层、更哲学、更嬉皮士的路线,结果一头扎进了机器。
这个选择很关键。Pascal 给了拉特纳两样东西:字符串类型、更强的抽象,以及第一次意识到“资源是要管理的”。真正改变他的却是 C。用 Pascal 的时候你写字符串,不用想它在内存里长什么样。用 C 的时候你必须想:这些东西是怎么被摆到内存里的。
他后来做的所有事情,都发生在这一层。
编译器在两头之间搭桥,两头都很麻烦
要理解拉特纳做了什么,得先理解编译器是什么。
他的定义很朴素:这是一道双边问题。一边是写代码的人,人不想写二进制,也不想为每一块硬件重新想一遍;另一边是跑程序的机器,机器的种类越来越多,x86、ARM、GPU,现在还有一堆专门为机器学习设计的加速器。编译器做的事,就是让人在想问题的抽象层级上思考,然后把他们写下的东西变成某块具体硬件能跑的东西。
“编译器的艺术,是让人类用他们想用的抽象层级去思考,然后让他们写的东西在某一块具体的硬件上跑起来。”(克里斯·拉特纳)
工程上的解法是把这个问题切成三段。前端负责解析某一种语言,C 的前端、C++ 的前端、JavaScript 的前端各不一样。中间是优化器。最后是后端,负责针对具体硬件生成机器码。
这样切是为了复用。编译器是极其庞大的软件,如果每种语言、每种硬件都要从头写一遍,成本谁都扛不住。所以大家把中间和后端标准化,让所有语言共用同一套优化和代码生成。LLVM 干的就是这件事。
今天跑在 LLVM 上的语言有一长串:Swift、Rust、Julia,还有 Clang 处理的 C、C++、Objective-C。它们语法差别巨大,语义模型完全不同,JavaScript 里“什么算假值”这件事和 C 里根本不是一回事,但这些差异全部留在前端。到了中间,它们变成同一种东西。
从语法树到控制流图:一个 for 循环的变形记
这是整期访谈里技术密度最高的一段,值得慢慢看。
前端解析完之后,代码变成一棵抽象语法树(AST)。你写了 a 加 b,树上就有一个加号节点,下面挂着两个子节点。你写了一次函数调用,树上就是一个 call 节点,挂着函数和参数。
然后开始“下降”(lowering)。语法树被转成中间表示,LLVM 用的那种叫控制流图(control flow graph)。
这里的差别用 for 循环最能说明。在语法树里,一个 for 节点下面挂着四个指针:初始化的表达式、条件的表达式、递增的表达式、循环体。层层嵌套,像俄罗斯套娃。
到了控制流图里,它变成几个基本块。一个块放初始化代码,一个块放循环体和递增,块与块之间用跳转连接。循环体执行完跳到条件判断,条件成立跳回循环体开头,不成立跳到出口。
每个块内部是一串直线排列的简单操作:这个加法,那个乘法,这里调用一次。没有嵌套,只有顺序和跳转。
为什么要费这么大劲换一种形状?因为这种形状更接近汇编,也更语言无关。JavaScript 那套让人抓狂的假值规则、Python 的隐式类型转换,全部可以留在前端处理掉。到了中间表示这一层,剩下的是纯粹的、干净的计算和跳转,所有语言共享同一套优化器。
编译器和神经网络图,像是同一个念头的两个版本
Fridman 在节目里问了一个很聪明的问题:你描述的这些东西,听起来和神经网络的图结构太像了。
拉特纳的回答点出了一个区别。
神经网络是层层不同的表示。输入进来是一层表示,经过一层变换变成另一种表示,越深层的表示越抽象。每一层都在做不同的事。
编译器是一种表示,反复变换。
同一个中间表示在编译器里被反复揉搓。找出循环里不随迭代变化的表达式提到外面去,找出重复计算,把 2 乘 x 换成左移一位,把常量提前算出来。这些变换是迭代进行的,一轮优化完了再来一轮,有时候一个 pass 会跑几十次。
两者都在做“变换表示”这件事,方向与层次完全不同。做机器学习系统的人可以从中拿到一个有用的判断:如果一层表示只被用一次,它大概是中间产物;如果一层表示被反复打磨几十轮,它才是真正的地基。
寄存器分配:编译器替你打的最硬的一仗
问编译器优化里最值钱的是什么,拉特纳直接说:寄存器分配。
场景是这样的。现代处理器里,内存慢,寄存器快,但寄存器数量极其有限。x86 在某些模式下只有十六个寄存器。而你写代码的时候随手就是几十个临时变量,循环里还套着循环,这段代码可能要执行几百万次。
如果循环里每个值都要往内存里读写一次,性能就废了。如果能把循环里所有活跃的值都塞进寄存器,速度会快一个数量级。
问题是怎么塞。同一个变量在函数的不同位置,活跃状态不一样。你在第三行定义了 x,在第五行用了它,之后再也不碰,那 x 在第五行之后就是“死的”,它占的寄存器可以给别人用。编译器要做的事情,是把整个程序看成一个有时间维度的图,然后在这个图里给每一个活着的值分配物理资源。
“你写的是 x,编译器看到的是四个不同的值,每一个都有它在整个函数里的生命周期,每一个都可能被放进寄存器、放进内存、或者在某些位置干脆重新算一遍,而不是从内存里读出来。”(克里斯·拉特纳)
这一步难在组合爆炸。可能的分配方案有无数种,而且这个决定会连锁影响后面所有的优化。寄存器分配做砸了,前面所有的优化都白搭。
另一场硬仗是指令调度。RISC 架构把流水线塞进了处理器,处理器能同时处理多条指令,前提是指令之间的顺序不能让流水线卡住。编译器的活是把指令重新排序,让流水线一直有活干。这一步的约束条件极其复杂,牵扯到各个指令的延迟、依赖关系、硬件端口数量。
这些技术都被研究了几十年,教材里都有。把一个算法变成能在几百人协作的代码库里稳定工作的实现,是另一回事。拉特纳反复强调,编译器的难点落在软件工程上。
让死对头坐到一张桌子上
LLVM 最了不起的地方,跟技术创新关系不大。
拉特纳自己说得很清楚:LLVM 里实现的各种算法,大部分在学术界早就有了,教科书上都写着。他的贡献是标准化,是把中间表示和后端做成一个大家都能用的公共层,然后把这件事做对了。
对到什么程度?Google 和苹果在同一个代码库上提交代码,AMD 和英特尔在同一个代码库上提交代码,英伟达和 AMD 在图形这部分提交代码。这些公司平时在法律诉讼和市场份额上打得不可开交。
它们为什么愿意合作?因为自己实现一遍太贵了,而且更缺的是人。编译器工程师是极难招的一类人,就算是大公司,也没办法为一个内部项目凑出一支顶级团队。共享基础设施是唯一理性选择。
更有意思的是这个基础设施被用到了它从没被设计过的地方。索尼把 LLVM 用在了电影制作的图形编译流程里,用拉特纳的话说,这让他们能做更好的特效。这说明分层做对了,接口抽象做得干净,才会有人拿它去做原设计者完全没想到的事。
反过来看 GCC。GCC 在它擅长的领域里非常强,但它是一个 C 编译器,一个基础设施。它的代码里塞满了全局变量,为特定用途设计,很难被复用到别的地方去。差别出在架构决策上,而这种差别要十年、二十年才显现出来。
社区治理也是同一套逻辑。今天 LLVM 有几百个贡献者,靠的是一套“代码所有者”制度。硬件厂商拥有自己硬件相关的那些代码,负责确保补丁被及时 review,负责这个方向上的架构决策。拉特纳名义上还在这个金字塔的顶端,他早就不 review 每一个补丁了,他做的事是调解技术分歧,保证整个社区在往前走。七年前还成立了 LLVM 基金会,管钱、管活动,不碰技术方向。
有意思的是,拉特纳在研究生阶段写的第一版寄存器分配器,后来被一个“比他聪明得多的人”整个换掉了。他把这件事当成 LLVM 设计成功的证据:模块化的好处就是别人能替换你的代码。
Clang 的起点,只是想把报错信息写清楚
2005 年拉特纳加入苹果。LLVM 在苹果内部落地,赶上了好几次大的架构迁移:转向英特尔、转向 64 位、iPhone 转向 ARM。因为是模块化的,每次迁移的成本都低得惊人。
开发者体验却很糟糕。当时的 Objective-C 程序员面对的报错信息、编译速度、工具链,都不怎么样。
然后他做了一件很拉特纳的事:谁也没告诉,下班之后自己写一个 C 编译器试试。
“我不会跟任何人承诺,也不告诉任何人。我就是晚上和周末开始写。”(克里斯·拉特纳)
写着写着他撞上了预处理器。这个东西人人都不喜欢,它极其复杂,里面塞着一堆古怪的历史包袱,比如三字符组(trigraph)这种鬼东西。而且它正是编译性能问题的一个关键症结。写到这里他发现,难和不可能之间有一道鸿沟,很多人一辈子没跨过去。
他跟经理说了。经理的反应是:太好了,这个问题我们确实要解决,我再给你一个人。
Clang 的目标定得很务实:更好的报错信息,更快的编译速度,还有 GCC 从来不支持的新的工具能力,比如重构工具和静态分析。要做到这些,编译器必须在内部维护大量额外的信息,还要保证这些信息不拖慢编译。这是个很精细的平衡。
C++ 是这里面最难啃的部分。规范文档厚得像砖头,语义规则之间的相互作用极其复杂。拉特纳的说法是:它几乎不可能实现,差一点就真的不可能。
跨过去的办法只有一个:一块一块地建,增量地做。还得招到真正懂各个子系统细节的工程师,一个人不可能同时精通模板推导、异常表、ABI 兼容性这些完全不同的深水区。
Swift 真正的阻力来自会议室
C++ 支持刚做完第一版,拉特纳脑子里冒出来一个念头:这语言有太多丑陋的地方,实现它的过程中你不可能不想到“一定存在更好的东西”。
于是他又一次开始了夜间项目。没有承诺,没有汇报,就是玩玩看。做完一些原型之后,他觉得这条路走得通,才去找当时的软件高级副总裁伯特兰·塞莱特(Bertrand Serlet)聊。塞莱特本身对语言有兴趣,给了不少早期指导,也鼓励他把这事推下去。之后才是告诉经理、告诉更多人。
技术上最难的部分已经过去了,剩下的是社会层面的阻力。
当时苹果内部的共识是:iPhone 之所以成功,是因为 Objective-C。这话背后有真实的文化支撑。当年苹果招软件工程师,招的就是喜欢 Objective-C 的人;很多高层和骨干工程师的文化记忆一路追溯到 NeXT 时代,那是 Objective-C 真正成型的年代。他们是“写 Objective-C 长大的”。
在这种氛围里提“我们做个新语言”,接近于异端。
拉特纳的观察角度不太一样。他觉得苹果内部喜欢 Objective-C 的人确实很多,最敢说话的那批人也确实是真爱,还有一大批人每天都在被这门语言的尖角割伤。它难学,它有各种奇怪的边界情况,它对新人不友好。
真正让这件事从“异端”变成“必须做”的,是一个词:内存安全。
Objective-C 的对象系统整个建立在 C 的指针之上,它本身就是 C 的一层扩展。指针不安全,这件事没得商量。只要你保留指针,你就没法在语言层面解决内存安全问题。你不能靠给 Objective-C 打补丁解决,因为指针就是它的地基。
这句话把讨论从“要不要”变成了“怎么做”。一旦承认 Objective-C 修不好,剩下的问题就是:新语言该长什么样?这完全是另一个阶段的工作了。
渐进式披露:为什么第一行代码必须能跑起来
Swift 的设计里,拉特纳最看重的一条原则来自 UI 设计:复杂度的渐进式披露。
它的意思是,一门语言应该允许人从最简单的入口进来。Swift 的第一课是打印一行 hello world。一行,没有 main 函数,没有头文件,没有 Java 那句 public static void main。Python 也是这样,这正是 Python 能成为编程第一课的原因。
然后往下走。加变量,写一句 var x = 4,什么叫变量,x 加 1 是什么意思。加控制流,if 怎么回事,for 怎么回事,while 怎么回事。加函数。加类。加泛型。加模块。每一层都比上一层复杂,每一层都能独立地跑起来。
“这在编译型语言里非常不典型。”(克里斯·拉特纳)
为什么编译型语言做不到这一点?因为它们必须在编译期知道所有类型、所有边界。这些信息对性能、对重构工具、对一切都有好处,代价是初学者一进门就要面对一堆声明和样板代码。Swift 想做的是保留静态类型的全部好处,同时把入口的门槛降到和 Python 一样低。
这件事的另一面是给不同的人留不同的层级。写库的人想要能钻到底层去抠细节,用库的人只想在高层拼装。一门语言如果只能服务其中一种人,它就废掉了一半的生态。
还有一个容易被忽略的点:Swift 不只做静态编译。它在 Colab 和 Jupyter 里被当作脚本语言用。你敲一行代码,背后发生的事情是这样的:先起一个 Unix 进程,然后每一行输入都过一遍 Swift 编译器前端、优化器、代码生成,最后把机器码注入到那个正在跑的进程里,边跑边替换。你在页面上敲代码的时候,它在原地打补丁。
“它能做到这件事,是因为设计时就考虑到了。”(克里斯·拉特纳)
拉特纳对编译这件事的看法也因此变得很不一样:静态编译、动态编译、解释执行,这些选项处在一条光谱上,彼此之间没有墙。把栈分好层,你就能在光谱上自由移动,不会把自己锁死在某一头。
一千二百行 Swift 代码,把 Python 拖了进来
Swift for TensorFlow 里有一个看起来像魔术的功能:在 Swift 里 import numpy,然后直接用。
拉特纳解释了实现原理,简单得出人意料。
一个动态类型语言可以有两种理解方式。大多数人会说 Python 没有类型。另一种理解是:Python 只有一种类型,就是那个 python object,所有东西都是它,正因为只有一种,类型才是隐式的。
Swift 有很多类型,数组、字符串、各种类。现在给它加上第二种:python object。
于是 import numpy 拿回来的是一个 python object,也就是那个模块。你调用 np 点 exp,Swift 做的事情是:告诉 Python 解释器“我要取这个对象上叫 exp 的成员”,拿回来又是一个 python object;再告诉解释器“把这个对象用这些参数调一下”,拿回来还是 python object。每一次都只是动态查表加一次跨界调用。
这一层胶水,纯 Swift 写的,只有一千二百行左右。它建在 Swift 跟 C 的互操作能力之上,因为跟 Python 解释器打交道本质上就是 C 层面的调用。
要让这一千二百行能写出来,Swift 语言本身必须先加两个特性:动态成员查找和动态调用。这两个特性的提案、实现、标准化、进入语言,花了一年。
用户看到的是一行 import。支撑这一行的是语言层面的新能力。这正是 Swift 这类有社区提案流程的语言能做到、纯库方案做不到的事。Python 那边为了机器学习加过什么语言特性?拉特纳想了一下,说大概只有 @ 这个矩阵乘法运算符。
TensorFlow 本质上是一个编译器,这句话值得展开
拉特纳提了一个视角,理解 TF 生态的混乱局面很有帮助。
TensorFlow 干的事情是:拿模型进来,让它在硬件上跑得飞快。这就是编译器干的事。它有前端,有优化器,有多个后端。
按这个框架看,Swift for TensorFlow 只是又一个前端。它和 Python、Go、Rust、Julia 的绑定共享同一套图表示、同一套运行时、同一套后端。
前端之间并不平等,它们分成三类。
第一类是 Python。它特殊,因为社区绝大部分的工程投入都在这边,它有自己的自动微分方案,有自己的 API 层。它的天花板也被这个位置限死了:你只能在 Python 这个库的边界之内做优化,改不了语言本身。
第二类是其他语言的绑定。它们就是包装层,调用 TF 的运行时 C API,没有自动微分,没有太多别的东西。
第三类是 Swift,只有它一个。它的思路是:既然我们可以改整条栈上的任何一个环节,那就从头问一遍,人和编译器之间的分工应该怎么切?
切法的核心在两处。第一处是类型系统。有了类型信息,编译器可以自动帮你构图,你不用手动想这些。汇聚、融合、优化,全都是白送的。第二处是自动微分,拉特纳认为这是 Swift for TensorFlow 最关键的贡献之一。
自动微分这件事的历史比大多数人想的要长。七十年代做数值计算的人写 Fortran,那时候就有一类工具叫源到源翻译器:你把自己的代码喂进去,它吐出来一份新的 Fortran 代码,里面带着你所有函数的导数。那是一整套技术积累,包括大量的优化技巧,包括怎么处理数值不稳定的各种花招。
问题在于,这些技术很难搬到今天的执行模型里。现代框架一次只能看到一个算子,看到一个加法、一个矩阵乘,而七十年代那批工具需要看到整个函数才能推理。当语言本身集成了自动微分,你就能重新打开这四十年的积累,把它们搬过来用。只是语言集成这件事本身也有一堆设计空间和复杂度要处理。
bfloat16:把比特挪个位置,性能就上去了
TPU 是软硬件协同设计最典型的样本,拉特纳在访谈里讲得挺细。
当时已经是第三代 TPU,整块液冷机箱的算力超过一百 petaflops。他的原话是“我们还没把想法用完”。
关键是那个数字格式:bfloat16。
它是 16 位浮点,但不是 IEEE 那种 16 位。标准半精度浮点把更多的位给了尾数,牺牲了指数范围。bfloat16 反过来,尾数更短,指数更长,和 32 位浮点一样长。
代价是精度更低。收益是能表示的范围和 float32 一样大。
为什么这个交换在机器学习里划算?因为训练过程中你会遇到极小的梯度,那些数字小到标准半精度直接下溢变成零,而它们恰恰是推动模型学习的东西。同时你也会遇到数量级很大的值。两头都要覆盖,中间精度松一点没关系。
更妙的是硬件成本。乘法器的面积和延迟跟尾数位数的平方成正比,跟指数位数是线性关系。把位从尾数挪到指数,硬件便宜了,模型效果没掉。
甚至还有理论说,精度降低反而能提升泛化能力。这个说法未必站得住,结果摆在那里:便宜、够用、有效。
这个格式的来历也挺有意思。它最初来自研究侧,那批人在想办法压缩网络传输中权重的体积,找各种压缩方案,后来这个方案被直接烧进了硅片。从一个传输优化的小技巧,变成了 TPU 性能的核心组成部分。
这就是拉特纳说的那个“只有 Google 能做的三合一”:底层编译器和芯片设计、上层软件框架、以及算法本身,三层可以同时改动。算法在变,硬件从设计到量产要几年,你必须押注几年后什么算法会流行,然后把晶体管花在最值的地方。
MLIR:LLVM 二十岁了,世界变了
访谈前三周,Google 在一个编译器会议上公布了一个新项目,MLIR(Multi-Level Intermediate Representation)。
背景是这样:TF 这条栈里嵌着一堆来自不同厂商的编译器。Google 有 XLA,英伟达有 TensorRT,英特尔有 nGraph。它们各自解决一部分问题,都在重复造轮子,都在想办法接进 TF。
MLIR 想做的是给这些子系统提供一层公共基础设施。第一步是让它们能插到一起,共享代码。更长远的目标是让整个行业开始协作,不要一遍一遍重新发明同样的东西。这套逻辑在传统编译器领域已经被验证过一次。
有人开玩笑说,MLIR 就是从 LLVM 学到的一切教训的 LLVM 版本,是一次修正错误的机会。拉特纳自己也承认这一点:LLVM 在它被设计出来要解决的问题上非常强,二十年过去,人们开始解决更高层的问题,需要新的技术。
这句话背后有一个很朴素的道理:一个好的抽象层会活得比它的设计者预期的更久,然后开始显出老态。这不是失败,成功的代价本来就是如此。
开源这件事,Google 和苹果的差别在哪
Fridman 问了一个直接的问题:苹果和 Google 在开源上的文化差异。
拉特纳的回答没有绕弯:他更认可 Google 的做法。他给了苹果一个公道的解释:苹果的文化有它的历史成因,那是三十五年前形成的路径,苹果也在适应。
他的论证是这样的:一家公司关心赚钱,这完全理性。真正让你赚钱的东西是什么?是那个做得极好的产品、那个差异化的体验,不是你的字符串库。把字符串库捂住不放,今天早就不是关键的那一层了。十五年前可能还是,今天的平台竞争不在那儿。
Google 开源 TensorFlow 这件事,拉特纳看来是一次“深刻的、极其聪明的洞察”。他特意设想了一个反事实的世界:一家公司说机器学习是我们的核心竞争力,我们不给别人。那个世界完全可能发生。TensorFlow 开源改变了整个机器学习领域,引发了一场独立的革命,同时在很多层面回馈了 Google 自己。
特斯拉那五个月:他respect愿景,不respect所有方法
2017 年前后,拉特纳在特斯拉待了大约五个月,职位是自动驾驶软件副总裁。他赶上的时间点是特斯拉从 Hardware 1 切到 Hardware 2,从第三方提供的视觉栈换成自研视觉栈。
他对这段经历的评价很坦率,也很体面。
Hardware 1 最初的设计目标其实很简单:交通感知巡航、车道保持这类功能。能在这个硬件上做到后来那种程度的辅助驾驶,他认为相当惊人。当时最大的挑战,是从第三方视觉栈往自研栈上迁。这一步时间压力很大,好在他可以沿用原来那支团队的知识和经验。
然后他说了一句很重的话:
我在特斯拉的时候,经历了也目睹了我此前在任何公司见过的最高的离职率,这有点让我吃惊。(克里斯·拉特纳)
紧接着,他说他从这段经历里学到并开始尊重的一件事:马斯克能吸引到极其优秀的人,因为他有非常清晰的未来图景,并且能让人相信那个图景。街角随便一个人都能喊“我们要去火星”,真正能让别人买账、并且真的把路径搭出来的人,极少。他对这种愿景的力量评价很高。
最后那句补充很关键:“我不认同他的所有方法,但我对这一点有极大的尊重。”(克里斯·拉特纳)
什么叫工作努力
Fridman 问了一个很少被认真回答的问题:工作努力到底指什么?
拉特纳先给了最常见的答案:很多小时。这是事实,他不否认。
他说真正难的是另一件事:同时保持短期交付的专注和长期的判断。
如果你眼里只有眼前这个任务,把它干完,然后只想着下一步,你会错过那个本该跳过去的下一个大坎。反过来,如果你天天只想着三年后的架构,你交付不出任何东西。在这两端之间来回摆,是最消耗人的。
他承认自己在苹果能做到这一点,靠的是搭起了团队和领导结构,让那些人能成长、能承担责任,从而把自己解放出来去“稍微疯一点”,去想下一件事。
还有一部分是经验带来的连接能力。做过足够多的事情之后,你能看到别人看不到的关联,那些关联本身能省掉大量的试错。他把这算作时间给的一笔复利:同样一个问题,二十年前他得从头推导,今天他能在几秒钟内认出这是哪一类坑。
底层还是大量的小时。他说自己对工作生活平衡有一套自己的理论,这理论是他对自己用的,不往团队身上套。他的目标说得很直白:改变世界,让它变好一点,这就是他的动力来源。
这段话里有个容易被忽略的分寸感。他谈的是自己愿意怎么活,没有把它包装成普适标准,更没有暗示别人不够努力。这种区分在技术圈其实挺稀缺。
那个龙
LLVM 的标志是一条龙。官方说法是龙代表力量、速度、智慧,也能代表流畅、优雅、模块化。真实原因简单得多:编译器领域有一本很老很经典的教材,叫《龙书》(Compilers: Principles, Techniques, and Tools)。
当年在苹果,大家做演示要放幻灯片,总得有个 LLVM 的图标,但没有。有人问,编译器技术该配个什么标志?拉特纳说,我不知道,龙大概是我们手上能拿到最好的东西了。苹果的设计团队把图标做了出来,社区接受了它,后来越来越多的设计师参与改进,就成了今天的样子。
有个细节挺有意思。拉特纳的妻子也是编译器工程师,她负责运营 LLVM 基金会,会去 Grace Hopper 大会发贴纸,想办法让更多女性对编译器产生兴趣。结果因为《权力的游戏》,大家都喜欢那个龙的贴纸,抢着要。文化偶尔会用这种方式帮上忙,让下一代工程师对编译器多看两眼。
一条原本只是为了填满幻灯片角落的龙,最后变成了一个跨公司、跨国籍、二十年的合作符号。这件事本身挺说明问题:基础设施的生命力来自被借用,不来自被保护。
把这一小时的信息用起来
这期访谈最有价值的地方不在结论,在那种看问题的方式。拉特纳反复在做同一件事:把复杂系统拆成层,让每一层都能被独立替换,然后耐心等十年看结果。这套方法可以迁移到很多地方。
- 设计任何长期系统时,先问“这一层会被谁替换”。拉特纳第一版寄存器分配器被别人整个换掉了,他当成好事。如果你的架构不允许这种替换发生,它就只能活到你的想法过时那天。
- 别用“这不可能”结束讨论,改成“这有多难”。Clang 和 Swift 都起于“我只是晚上写写看”。区别在于他撞墙之后去判断的是难度等级,可行性压根不是他问的问题。
- 遇到僵局时,去找那个修不好的地基。Swift 能立项靠的是一个不可辩驳的限制条件:指针就是内存安全问题的根,打补丁永远修不好。把争论从“要不要”转成“怎么做”,只需要这一句话。
- 把渐进式披露用到文档、API、产品上。第一行代码必须能跑起来,然后才是第二层复杂度。任何要求用户一进门就理解全貌的设计,都会把大批人挡在门外。
- 做软硬件或跨层项目时,接受“押注”这件事。硬件从设计到量产要几年,算法几个月就换一轮。bfloat16 之所以能成,是因为有人敢在不确定的情况下押注“指数比尾数更重要”,并且这个判断在模型层面站得住。
- 评估一个开源项目,先看它有没有被用在设计之外的地方。索尼拿 LLVM 做电影特效的图形编译,这个信号比任何性能基准都更能说明它的分层是对的。
- 判断一个基础设施值不值得做,看重复成本。英特尔、AMD、英伟达、Google、苹果愿意在一个代码库上合作,动力来自两个硬约束:自己实现一遍太贵,招不到足够的人。找到别人绕不过去的高成本,就是协作的入口。
- 关于工作强度:不要只优化小时数。真正稀缺的能力是在短期交付和长期判断之间来回切换。把执行层交给能成长的人,是唯一能让自己腾出手去想下一件事的办法。
最后留一句拉特纳没有明说、但整期访谈都在示范的事:他从研究生到 2019 年,做的所有项目都有一个共同点,起点小得几乎不体面。一个夜间项目,一本教材上的程序,一个只想读一年硕士就回去工作的人。值得做的事,往往一开始看起来不值得向任何人汇报。
本集资源与出处
- 视频在 YouTube 收看
- 音频官方 MP3 直链来自 lexfridman.com 官方播客 RSS
- 官网Lex Fridman 官网本集页面
- 文稿HappyScribe 版文稿