为什么需要
计算机只能执行机器码(二进制指令)。
而人类写代码用的是高级语言。编译器和解释器就是把高级语言转成可执行形式的中介。
编译器(Compiler)
工作方式:把整个源程序一次性翻译成目标代码(机器码或中间代码),然后执行。
流程(简化):
- 词法分析:把字符流切分成 token(关键字、标识符、运算符等);
- 语法分析:根据语法规则构建抽象语法树(AST);
- 语义分析:类型检查、作用域检查等;
- 中间代码生成;
- 优化:常量折叠、死代码消除、循环优化、内联等;
- 目标代码生成:生成机器码(或汇编);
- 链接:把多个目标文件和库合并成可执行文件。
代表:C、C++、Rust、Go。
优点:执行快(编译期完成优化,运行时直接执行机器码)。
缺点:编译需要时间;跨平台需要重新编译。
解释器(Interpreter)
工作方式:逐行读取源代码,边解释边执行。
代表(经典):早期的 BASIC、Shell 脚本。
优点:启动快,跨平台容易(同一份源码在不同平台的解释器上运行)。
缺点:执行慢(每次执行都要重新分析源码)。
现代语言:混合方案
纯粹的编译或解释已经少见,主流是混合:
一、字节码 + 虚拟机
代表:Java(JVM)、Python(CPython)、C#(.NET CLR)。
流程:
- 源代码编译成字节码(Bytecode)——一种平台无关的中间表示;
- 虚拟机(VM)解释执行字节码,或进一步编译成机器码。
优势:
- 跨平台:字节码是统一的,各平台只需实现虚拟机("Write Once, Run Anywhere");
- 比纯解释快(字节码更接近机器指令,省去了重复的语法分析)。
二、即时编译(JIT,Just-In-Time)
思路:运行时把"热点代码"编译成机器码。
流程:
- 先解释执行(或执行字节码);
- 监控哪些代码被频繁执行(热点);
- 把这些热点代码动态编译成本地机器码;
- 后续执行直接使用编译后的机器码。
代表:Java HotSpot JVM、JavaScript 的 V8 引擎、.NET。
优势:
- 结合了编译的速度和解释的灵活性;
- 可以进行基于运行时信息的优化(这是静态编译做不到的)——比如发现某个函数在运行时总是收到整数参数,就生成针对整数的特化版本。
代价:启动时需要"预热"(JIT 编译需要时间),因此短生命周期的程序(如一次性脚本)可能感觉不到 JIT 的好处。
三、提前编译(AOT,Ahead-Of-Time)
与 JIT 相对:在程序运行之前就编译成机器码。
代表:Go、Rust、C/C++,以及 Java 的 GraalVM Native Image、.NET 的 AOT。
优势:启动快(无需预热),适合短生命周期进程和Serverless 场景(冷启动时间敏感)。
一个对比
| 特性 | 纯编译 | 纯解释 | 字节码+JIT |
|---|---|---|---|
| 执行速度 | 最快 | 慢 | 接近编译(预热后) |
| 启动速度 | 慢(要编译) | 快 | 中等(要预热) |
| 跨平台 | 差 | 好 | 好 |
| 运行时优化 | 无 | 无 | 有 |
一个实用含义
Python 慢的原因:CPython 是解释执行字节码的,且是动态类型——每次操作都要做类型检查和方法查找。
优化路径:
- PyPy:带 JIT 的 Python 实现,对纯 Python 代码可达数倍加速;
- NumPy:把密集计算交给底层的 C/Fortran 实现;
- Cython / C 扩展:把热点代码编译成机器码;
- Numba:用装饰器把数值函数 JIT 编译。
说明:本文为编译原理的知识整理。