逐行读取源代码,边解析边执行,不产生独立可执行文件。每次运行都要重新解析。
- 启动快,无需预编译
- 跨平台,代码可直接分发
- 运行时动态类型检查
- 执行速度相对较慢
- 代表:Python、Ruby、JavaScript(解释模式)
将整个源文件一次性翻译为机器码,产生独立可执行文件,之后运行无需源码。
- 执行速度极快,直接运行机器码
- 编译时完整类型检查
- 分发单一二进制文件
- 编译过程需要时间
- 代表:Go、C、C++、Rust
| 维度 | 解释器 | 编译器 |
|---|---|---|
| 翻译时机 | 运行时逐行翻译 | 运行前整体翻译 |
| 执行产物 | 无独立产物(或字节码) | 独立可执行二进制 |
| 启动速度 | 快 ✓ | 慢(需先编译)✗ |
| 执行速度 | 慢 ✗ | 极快 ✓ |
| 错误发现 | 运行时才发现 | 编译期发现 |
| 跨平台 | 源码直接跨平台 | 需为不同平台交叉编译 |
| 内存占用 | 较高(需运行时环境) | 较低(无运行时依赖) |
| 调试体验 | REPL 交互式调试 | 需编译后调试 |
.pyc 字节码再解释执行(CPython),
Go 虽是编译型但编译极快(秒级),JavaScript 的 V8 引擎使用 JIT(即时编译)兼具两者优势。
.py → ② 词法分析
Tokenizer → ③ 语法分析
Parser→AST → ④ 符号表
编译前检查 → ⑤ 字节码编译
compile() → ⑥ PVM执行
eval_breaker
__pycache__/xxx.pyc,
下次运行时若源码未变则跳过前5步,直接从字节码开始执行。
词法分析(Tokenization)
将源码字符流分割成一个个 Token(记号),是所有后续处理的基础。
def add(a, b): return a + b result = add(3, 5) print(result)
NAME 'def' NAME 'add' OP '(' NAME 'a' OP ',' NAME 'b' OP ')' OP ':' NEWLINE '\n' INDENT '' NAME 'return' NAME 'a' OP '+' NAME 'b' ...
tokenize 模块,可以用 python -m tokenize script.py 查看 Token 流。语法分析 → 抽象语法树 (AST)
Parser 将 Token 流按语法规则构建成树形结构,表达代码的语义层次关系。
return a + b
import ast src = "return a + b" tree = ast.parse(src, mode='single') print(ast.dump(tree, indent=2))
# 宏、装饰器、代码注入都依赖AST class Transformer(ast.NodeTransformer): def visit_Add(self, node): return ast.Mult() # 把+换成*
字节码编译(Bytecode Compilation)
AST 被编译为 CPython 字节码(bytecode)—— 一种针对 Python 虚拟机的中间指令集,存储在 code object 中。
def add(a, b): return a + b
import dis dis.dis(add)
Python 虚拟机执行(PVM / ceval.c)
CPython 的核心是 ceval.c 中的求值循环,本质是一个巨大的 switch-case,
逐条取出字节码指令并执行对应的 C 代码。
/* CPython ceval.c 核心循环(简化版)*/ for (;;) { opcode = NEXTOPCODE(); // 取下一条字节码 oparg = NEXTOPARG(); // 取操作数 switch (opcode) { case LOAD_FAST: v = GETLOCAL(oparg); // 从局部变量表取值 PUSH(v); // 压栈 break; case BINARY_OP: right = POP(); left = POP(); result = PyNumber_Add(left, right); // 调C函数 PUSH(result); break; case RETURN_VALUE: retval = POP(); goto return_or_yield; // ... 400+ 种指令 } }
执行栈模型
- 基于栈(Stack-based VM)
- 每个函数调用有独立的
frame - frame 包含:本地变量、栈空间、字节码指针
- GIL 全局解释器锁保证线程安全
GIL 的影响
- 同一时刻只有一个线程执行 Python 字节码
- I/O 密集型:线程可切换,影响小
- CPU 密集型:多线程无法并行,用多进程代替
- Python 3.13+ 支持 free-threaded 模式(实验性)
PyPy 在 CPython 字节码解释的基础上增加了 JIT(Just-In-Time)编译: 当某段代码被反复执行(热点代码)时,动态编译为机器码缓存,后续直接执行,无需再经过虚拟机。
(冷代码) → Tracing JIT
检测热点 → 机器码
(热代码缓存) → 直接CPU执行
.go → ② 词法/语法分析
scanner+parser → ③ AST构建
syntax.Parse → ④ 类型检查
typecheck → ⑤ IR中间表示
SSA → ⑥ 优化
内联/逃逸分析 → ⑦ 代码生成
汇编 → ⑧ 链接
ELF/Mach-O
词法分析 + 语法分析
Go 编译器在 cmd/compile/internal/syntax 包中实现,词法语法分析合并处理,直接构建 AST。
package main import "fmt" func add(a, b int) int { return a + b } func main() { result := add(3, 5) fmt.Println(result) }
类型检查(Type Checking)
Go 在编译期进行严格的静态类型检查,是其相比 Python 的重大优势之一。
func main() { var x int = 10 var s string = "hello" // ❌ 编译期报错:类型不匹配 result := x + s // 必须显式类型转换 result2 := string(x) + s }
./main.go:8:18: invalid operation:
x + s (mismatched types int and string)
x = 10 s = "hello" # 语法没问题,执行时报错 result = x + s
TypeError: unsupported operand type(s)
for +: 'int' and 'str'
(运行到这行才知道出错!)
SSA 中间表示(Static Single Assignment)
AST 经类型检查后转换为 SSA 形式的中间代码。SSA 要求每个变量只赋值一次,方便优化器分析数据流。
可通过 GOSSAFUNC=add go build 查看。
func add(a, b int) int { c := a + b return c }
// 每个变量只有一次定义 b1: // 入口块 v1 = LocalAddr &a v2 = LocalAddr &b v3 = Load v1 v4 = Load v2 v5 = Add64 v3 v4 // c = a + b Return v5 // return c
编译器优化策略
Go 编译器在 SSA 阶段应用多种优化 Pass,通常可执行 40+ 次转换。
内联(Inlining)
// 源码:函数调用有开销 func double(x int) int { return x * 2 } func main() { y := double(5) // 简单函数 } // 内联后:消除函数调用开销 func main() { y := 5 * 2 // 直接展开 }
逃逸分析(Escape Analysis)
// 决定变量分配在栈还是堆 func noEscape() { x := 42 // 栈分配,函数结束自动释放 _ = x } func escape() *int { x := 42 return &x // x 逃逸到堆,GC 管理 } // go build -gcflags="-m" 查看逃逸分析
常量折叠
// 编译期直接计算常量表达式 const ( KB = 1024 MB = KB * 1024 // 编译期 = 1048576 GB = MB * 1024 // 编译期 = 1073741824 )
死代码消除
func foo() { if false { // 永假,整块删除 expensive() } return unreachable() // return后,删除 }
边界检查消除
// 切片访问默认有边界检查 s := []int{1, 2, 3} // 编译器能证明 i < len(s) 时 // 自动消除运行时边界检查 for i := range s { _ = s[i] }
机器码生成 & 链接
SSA 最终被转换为目标架构(AMD64/ARM64等)的汇编,再由汇编器产生目标文件,最后链接为可执行文件。
func add(a, b int) int { return a + b }
go tool compile -S main.go
// add(a, b int) int TEXT main.add(SB) MOVQ "".a+8(SP), AX // AX = a MOVQ "".b+16(SP), CX // CX = b ADDQ CX, AX // AX += CX MOVQ AX, "".~r0+24(SP) // return AX RET
scp 到任何相同架构的机器即可运行。这也是 Go 容器镜像可以 FROM scratch 的原因。
交叉编译
# 在 Mac 编译 Linux 二进制 GOOS=linux GOARCH=amd64 \ go build -o app-linux main.go # 编译 ARM64(如树莓派/M芯片) GOOS=linux GOARCH=arm64 \ go build -o app-arm64 main.go # Windows 可执行文件 GOOS=windows GOARCH=amd64 \ go build -o app.exe main.go
编译速度秘诀
- 包级别增量编译,只重编改动包
- 语法设计避免回溯(无循环依赖)
- 显式 import,无隐式包加载
- 包并行编译
- 结果:百万行项目秒级编译
- 每次运行都经过词法→语法→编译步骤(有 .pyc 缓存除外)
- 字节码是跨平台的,但 PVM 是平台特定的
- 动态类型:
a + b的实际行为运行时才确定 - 每条字节码执行需要多个 C 函数调用
- 编译一次,运行多次,无需解释开销
- 类型信息在编译期已消耗,运行时无需类型检查
a + b在编译期已知是ADDQ指令- CPU 直接执行机器码,速度接近 C
def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2) fib(40) # ≈ 35 秒(CPython) # ≈ 8 秒(PyPy)
func fib(n int) int { if n <= 1 { return n } return fib(n-1) + fib(n-2) } fib(40) // ≈ 0.5 秒 // 快约 70 倍
| 错误类型 | Python(解释型) | Go(编译型) |
|---|---|---|
| 类型错误 | 运行时崩溃 ✗ | 编译期报错 ✓ |
| 未定义变量 | 运行时 NameError ✗ | 编译期 undefined ✓ |
| 接口未实现 | 运行时 AttributeError ✗ | 编译期 does not implement ✓ |
| 语法错误 | 加载时 SyntaxError ⚡ | 编译期 ✓ |
| 导入缺失 | 运行时 ModuleNotFoundError ✗ | 编译期 cannot find package ✓ |
| 未使用导入 | 无警告(浪费内存) | 编译错误(强制整洁)✓ |
- 🤖 AI/ML 开发:NumPy、PyTorch、TensorFlow 生态
- 📊 数据分析:Pandas、Jupyter、快速探索
- 🕷️ 脚本 & 自动化:运维脚本、爬虫
- 🌐 Web 后端(中低流量):Django、FastAPI
- 🔬 科学计算:SciPy、Matplotlib
- ⚡ 快速原型:动态类型利于迭代
- ☁️ 云原生 & 微服务:Docker、K8s、Istio 都用 Go 写
- 🌐 高并发 Web 服务:goroutine 轻量并发
- 🔧 CLI 工具:单二进制,部署极简
- ⚡ 高性能网络服务:API 网关、代理
- 📦 系统工具:跨平台编译,无依赖
- 🏗️ 大型工程:强类型保障代码质量
| 语言 | 模式 | 说明 |
|---|---|---|
| Python (CPython) | 编译到字节码 + 解释 | 源码→字节码(.pyc)→PVM 解释执行 |
| Python (PyPy) | 字节码 + JIT | 热点代码动态编译为机器码,快 3-5x |
| JavaScript (V8) | 解析 + JIT | Ignition 解释 + TurboFan JIT,接近编译型速度 |
| Java | 编译到字节码 + JIT | javac→.class→JVM JIT 编译热点,跨平台 |
| Go | AOT 编译 | 一次编译,无运行时解释,速度接近 C |
| Rust | AOT 编译 + LLVM | 无 GC,零成本抽象,性能最高但学习曲线陡 |
编译器(Go): 「全部翻译再执行」,安全高效,部署简单,适合高性能服务和工程化大项目。
两者没有绝对优劣之分,而是不同场景下的工程权衡。 现代语言(JIT、逐步类型化)正在模糊这条边界。