为什么真语言要编译:字节码登场
卷 IV 那台解释器能跑,已经很了不起了。但它有个天生的软肋:慢。慢在哪、为什么慢、真语言怎么绕开 —— 这一章讲清这三件事。答案是一条几乎所有工业级语言都走的路:别再每次运行都去爬那棵树了,先把树压平成一串线性指令(字节码),再交给一台极简的虚拟机去跑。这一章讲「为什么」,下一章造那台机器。
树遍历为什么慢
我们那台树遍历求值器,优雅、好懂,但每算一个表达式,它都在干一堆「隐形的苦力活」:
- 到处追指针。那棵语法树的节点,散落在内存各处,靠指针互相连着。求值时,CPU 得跟着这些指针在内存里东一下西一下地跳 —— 而这种「跳着访问内存」对现代 CPU 极不友好(缓存命中率低,每次跳都可能等内存)。
- 反复判断「这是什么节点」。每进一个节点,都要
switch (node.k)判断一次类型、走对应分支。同一棵树跑一万遍,这个判断就白做一万遍。 - 每次运行都从头爬。循环体里的表达式,循环多少次就重新遍历多少次那棵子树 —— 结构分析的活,一遍遍地重做。
换句话说:树遍历把「理解结构」和「执行」搅在了一起,每次执行都要重新理解一遍结构。对跑一次就完的脚本无所谓,对要跑几百万次的热循环,这是灾难。
关键一招:把树压平成一条直线
能不能把「理解结构」这件事只做一次,产出一个「已经不需要再理解、照着执行就行」的东西?能。这个东西就是字节码(bytecode) —— 一串线性的、扁平的指令。
拿 1 + 2 * 3 举例。它的树是二维的、嵌套的。压平成字节码,变成一串从上往下读的指令:
CONST 1 // 把常量 1 压到栈上 CONST 2 // 把常量 2 压上去 CONST 3 // 把常量 3 压上去 MUL // 弹出 2 和 3,相乘,把 6 压回去 ADD // 弹出 1 和 6,相加,把 7 压回去
看出门道了吗?树里「先算 2*3 再加 1」那个嵌套的层次,被压平成了指令的先后顺序 —— MUL 排在 ADD 前面,乘法就自然先发生。二维的结构信息,被编码进了一维的指令序列。执行的时候,只要从上往下老实读这串指令,不用再爬任何树、不用再判断任何节点类型。
这就是「编译」的本质,一句话说清:把那些「每次运行都要重做、但其实做一次就够」的分析工作,提前做完,固化成一种照着跑就行的形式。
树遍历:每次运行都要 switch 判断节点、追指针爬树。字节码:这些在编译时一次性做完,运行时只剩「读指令、执行」这种笨而快的活。翻译一次,运行千万次 —— 循环跑得越多,这笔预付的「编译成本」摊得越薄,越划算。这也是为什么它叫「编译型」:重头戏在运行之前就演完了。
那台要跑字节码的机器:一个栈
字节码是给谁跑的?给一台虚拟机(Virtual Machine, VM)。之所以叫「虚拟」,是因为它不是真实的 CPU 硬件,而是一个用软件模拟出来的、极其简单的处理器。
最常见、也是我们要造的这种,叫栈式虚拟机(stack machine)。它的全部家当,就是一个栈(还记得第 14 章的栈吗,这里是另一个栈 —— 专门用来放运算的中间结果)。上面那串字节码的每条指令,都是在对这个栈做「压」和「弹」:
CONST 2:把 2 压到栈顶。MUL:从栈顶弹出两个数,相乘,把结果压回去。
就这两个动作 —— 压、弹 —— 加上「从上往下读指令」,一台能算任意表达式的机器就成了。它简单到近乎愚蠢,而「愚蠢」正是它快的原因:没有分支预测失败、没有指针乱跳、没有类型判断,就是一条直线跑到底。下一章我们就把这台机器造出来。
回流
「编译成字节码,再由虚拟机执行」不是什么高冷技术 —— 你每天用的语言,过半都是这么跑的。
Java / Kotlin:你的 .kt 被 javac / kotlinc 编译成 .class 里的 JVM 字节码,再由 JVM(一台栈式虚拟机!)执行。你反编译看到的那些 iload、iadd、invokevirtual,就是这一章说的字节码,而且 JVM 正是栈机器 —— 它们也在栈上压来弹去。
Python:你 import 一个模块时生成的 .pyc 文件,里面就是 Python 字节码;import dis; dis.dis(f) 能把任意函数的字节码打出来给你看 —— 你会看到 LOAD_CONST、BINARY_ADD 这些,和本章的 CONST、ADD 几乎一模一样。
WebAssembly:近几年最火的「字节码」,让 C / Rust 编译成一种能在浏览器里跑的紧凑指令格式 —— 它也是个栈机器。你现在理解的这套「压平成指令、栈机器执行」的模型,直接就是 WASM 的心智模型。
下次有人说某语言「跑在虚拟机上」「编译成字节码」,你不会再觉得那是黑话 —— 你知道它指的就是这一章、和接下来两章要造的东西。
这一章的一句话
树遍历慢在每次运行都要重新爬树;真语言的对策是编译 —— 把树压平成一串线性字节码,让「理解结构」只做一次,再交给一台极简的栈式虚拟机去笨而快地执行。
下一章,我们造那台栈机器 —— 你会看到一串指令,是怎么在一个栈上压压弹弹,算出结果的。