编程语言 编程语言是人类意图与机器执行之间的接口。本文件涵盖语言范式、类型系统、内存管理策略、编译流水线、解释与 JIT 编译、关键语言特性、领域特定语言以及设计权衡。 每一款软件、每一个 ML 模型、每一个操作系统都是用某种编程语言写成的。但有数百种语言,各有所长。为什么?因为语言设计涉及根本性的权衡:性能对安全、表达力对简洁、控制力对抽象。理解这些权衡能帮你为任务选对工具,并理解你所处的约束。 语言范式 范式(paradigm)是一种编程风格:一套指导你如何组织代码、如何思考问题的原则。 命令式(imperative)编程把计算描述成一串改变状态的命令。"把 x 设为 5。给 x 加 3。如果 x > 7,就打印它。"C、Python、Java 本质上都是命令式的。
编程语言是人类意图与机器执行之间的接口。本文件涵盖语言范式、类型系统、内存管理策略、编译流水线、解释与 JIT 编译、关键语言特性、领域特定语言以及设计权衡。
**范式(paradigm)**是一种编程风格:一套指导你如何组织代码、如何思考问题的原则。
**命令式(imperative)**编程把计算描述成一串改变状态的命令。"把 x 设为 5。给 x 加 3。如果 x > 7,就打印它。"C、Python、Java 本质上都是命令式的。其心智模型是一台你逐步修改其内容的带内存的机器。
面向对象(object-oriented,OOP)编程围绕对象(objects)组织代码:把数据(属性)和行为(方法)打包在一起。对象之间通过互相发消息来交互。关键思想是封装(encapsulation)(把内部状态藏在公开接口背后)、继承(inheritance)(通过扩展已有类来创建新类)和多态(polymorphism)(通过共同接口统一对待不同类型)。Java、C++、Python 都支持 OOP。
函数式编程(functional programming,FP)把计算看作对数学函数的求值。核心原则:不可变性(immutability)(数据一旦创建就不再改变)、纯函数(pure functions)(输出只取决于输入,没有副作用)和一等函数(first-class functions)(函数是值,可以作为参数传递、从其它函数返回、存进变量)。Haskell 是纯函数式的。Python、JavaScript、Scala 支持函数式风格。
纯函数容易推理、测试和并行化(没有共享可变状态就意味着没有竞态条件)。这就是为什么函数式思想在分布式系统和数据管道中越来越多地被采用。本书通篇使用的 JAX 就是函数式的:jax.grad 之所以能用,正是因为 JAX 的函数是纯函数。
**逻辑式编程(logic programming)**描述什么应当为真,而不是如何计算它。你陈述事实和规则,运行时去找解答。Prolog 是经典例子:给定"苏格拉底是人"和"所有人都会死",引擎推导出"苏格拉底会死"。逻辑式编程用于 AI 知识库和类型检查。
大多数现代语言是**多范式(multi-paradigm)**的:Python 同时支持命令式、OOP 和函数式风格;Rust 支持命令式和函数式。范式是一种工具,不是一种信仰。
**类型(type)**给值分类,并决定哪些操作是合法的。整数 3 和字符串 "3" 是不同的类型:你可以把整数相加,但不能把字符串相加(好吧,你可以拼接字符串,但那是另一种操作)。
静态类型(static typing):类型在程序运行之前的编译期检查。类型错误能被早早抓住。C、Java、Rust、Go 是静态类型的。你必须声明类型(或由编译器推断):
let x: i32 = 5; // Rust: x 是 32 位整数 let y: f64 = 3.14; // y 是 64 位浮点数 // let z = x + y; // 编译错误:不能把 i32 和 f64 相加
x = 5 # 此刻 x 是 int x = "hello" # 现在 x 是字符串——不报错
强类型(strong typing):语言阻止隐式类型转换。Python 是强类型的:"3" + 5 会抛 TypeError。弱类型(weak typing):语言会悄悄转换类型。JavaScript 是弱类型的:"3" + 5 得到 "35"(数字被转成字符串)。C 也是弱类型的:你可以把指针强制转换成整数。
**类型推断(type inference)**让编译器无需显式标注就能推出类型:
let x = 5; // 编译器推断:i32 let y = x + 3.0; // 编译错误:即使有推断,也不能混用类型
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut max = &list[0]; for item in &list[1..] { if item > max { max = item; } } max } // 适用于整数、浮点数、字符串——任何支持比较的类型
def train(model: nn.Module, lr: float) -> float)和静态分析工具(mypy),在部署前抓住错误。PyTorch 和 JAX 用 Python 换取灵活性;TensorRT 和 ONNX Runtime 用 C++ 换取性能。**栈(stack)**存放局部变量和函数调用帧。分配很简单(移动栈指针),释放是自动的(函数返回时弹出帧)。栈访问很快,因为它总在缓存里。但栈大小固定(通常 1-8 MB),且只支持 LIFO(后进先出)分配。
**堆(heap)**存放动态分配的数据(对象、数组、大小在编译期未知的字符串)。堆分配更慢(需要找到一块空闲块),且需要显式或自动释放。堆可以增长到填满可用内存。
手动内存管理(manual memory management,C、C++):程序员显式地分配(malloc)和释放(free)堆内存。控制力和性能最大化,但极易出错:
垃圾回收(garbage collection,GC):运行时自动检测并释放不再可达的内存。程序员永远不用调用 free。
追踪式 GC(tracing GC,Java、Go、Python 的循环垃圾回收器):周期性地从"根"(栈变量、全局变量)出发遍历所有可达对象,释放不可达的对象。简单但会产生 GC 暂停(GC pauses):回收器运行时程序停下。现代回收器(Go 的并发 GC、Java 的 ZGC)把暂停时间压到亚毫秒级。
引用计数(reference counting,Python 的主要机制,Swift、Objective-C):每个对象跟踪有多少引用指向自己。计数降到 0 时立即释放。没有暂停,但处理不了循环引用(cycles)(A 引用 B,B 引用 A,两者计数都大于 0 但都不可达)。Python 用一个独立的循环检测器来处理这种情况。
所有权(ownership,Rust):编译器在编译期强制内存安全规则,零运行时开销。
**借用检查器(borrow checker)**是 Rust 的杀手锏,也是最陡的学习曲线。它在不依赖垃圾回收的前提下保证了内存安全和线程安全,这也是为什么 Rust 越来越多地用于性能攸关的系统(OS 内核、游戏引擎、像 Candle 和 Burn 这样的 ML 推理运行时)。
词法分析(lexing,分词 tokenisation):把源代码文本转换成一串词法单元(token)。x = 3 + y 变成 [IDENT("x"), EQUALS, INT(3), PLUS, IDENT("y")]。词法分析器剥去空白和注释。
语法分析(parsing):从 token 流构建一棵抽象语法树(Abstract Syntax Tree,AST)。AST 表示程序的层次结构。3 + y * 2 被解析成 Add(3, Mul(y, 2))(乘法优先级更高)。语法分析器检查语法:括号不匹配、缺少分号都在这里被抓出来。
语义分析(semantic analysis):检查类型、解析变量名、核实函数调用参数是否正确。静态类型检查就在这里发生。输出是一棵带类型的、有标注的 AST。
优化(optimisation):在不改变程序行为的前提下改造它,使之运行更快。常见优化:
3 + 5,替换成 8。代码生成(code generation):把优化后的表示翻译成目标机器码(x86、ARM)或某种中间表示。
**解释器(interpreter)**逐行(或逐条语句)执行程序,不生成机器码。这让启动很快、开发交互性很强,但执行更慢(每一行每次运行都要被重新分析)。
大多数解释型语言其实会先编译成字节码(bytecode):一种比源代码简单但与机器无关的中间表示。字节码在**虚拟机(virtual machine,VM)**上运行。
CPython(标准的 Python 实现)把 Python 源码编译成字节码(.pyc 文件),由 CPython 虚拟机执行。VM 逐条解释字节码。这就是为什么对计算密集型代码 Python 比 C 慢约 100 倍。
JVM(Java 虚拟机,Java Virtual Machine):Java 编译成 JVM 字节码(.class 文件)。JVM 先解释字节码,再把频繁执行的代码路径("热点")JIT 编译成本地机器码。这就是为什么 Java 启动比 C 慢(解释开销),但对长时间运行的程序却能逼近 C 的速度(被 JIT 优化过的热点路径)。
**JIT(即时编译,Just-In-Time compilation)**在运行期把代码编译成机器码,利用只在执行期间才能获得的信息。JIT 可以基于实际运行数据做优化:如果某个函数总是被用整数参数调用,JIT 就生成只针对整数的专用机器码,跳过类型检查。
PyPy 是带 JIT 编译器的另一种 Python 实现。通过把热点循环 JIT 编译成机器码,它跑大多数 Python 代码比 CPython 快 5-10 倍。但它对 C 扩展模块(NumPy、PyTorch)的兼容性有限,这限制了它在 ML 中的使用。
从解释到编译的光谱不是非此即彼的:
jax.jit 在首次调用时把 Python 函数编译成优化过的 XLA 代码,之后缓存编译好的版本。def make_adder(n): def add(x): return x + n # n 捕获自外层作用域 return add add5 = make_adder(5) print(add5(3)) # 8
闭包是回调、装饰器和偏应用(partial application)背后的机制。它是函数式编程的基石。
模式匹配(pattern matching):一种强大的控制流机制,能解构数据并根据其形状分支:
match value { Some(x) if x > 0 => println!("Positive: {}", x), Some(0) => println!("Zero"), Some(x) => println!("Negative: {}", x), None => println!("Nothing"), }
模式匹配比 if-else 链表达力更强:它检查数据的结构(是 Some 还是 None?是否包含满足某条件的值?),而不仅仅是相等。Python 在 3.10 中加入了结构化模式匹配(match/case)。
代数数据类型(algebraic data types,ADTs):可以是若干种变体之一、每种携带不同数据的类型。Result 类型要么是 Ok(value) 要么是 Err(error)。Tree 要么是 Leaf(value) 要么是 Node(left, right)。ADT 与模式匹配结合,可以穷尽地处理所有情形,消灭一整类 bug(空指针异常、未处理的错误码)。
特质与接口(traits and interfaces):定义一个类型必须实现的一组方法,但不规定如何实现。这带来了多态:一个接受"任何实现了 Display 特质的类型"的函数,对整数、字符串和自定义类型都适用。Rust 用特质,Java 用接口,Go 用隐式接口,Python 用鸭子类型("如果它走起路来像鸭子……")。
**领域特定语言(domain-specific language,DSL)**是为某个特定问题领域设计的语言,用通用性换取在该领域内的表达力。
SQL:关系数据库的语言。SELECT name FROM users WHERE age > 30 比等价的命令式循环可读得多,也更容易优化。数据库引擎优化查询执行计划,自动选择连接策略和索引使用方式。
正则表达式(regular expressions):一种用于文本中模式匹配的小语言。\d{3}-\d{4} 匹配像 "555-1234" 这样的电话号码。正则引擎把模式编译成有限自动机以高效匹配。
着色器语言(shader languages,GLSL、HLSL、Metal Shading Language):运行在 GPU 核上、用来计算像素颜色、顶点位置或进行计算操作的程序。着色器是大规模并行的:每次调用独立处理一个像素或一个元素。这与 CUDA 用于 ML 计算的执行模型相同。
在 ML 中,像 PyTorch 和 JAX 这样的框架本质上是嵌入在 Python 中的、面向张量计算的 DSL。它们提供领域专用抽象(张量、自动微分、设备放置),同时借助 Python 的生态系统。
没有哪种语言在所有方面都是最好的。设计就是选择做哪些权衡:
性能对安全:C 给你原始速度和硬件控制,但任由你破坏内存。Rust 给你接近的速度,并在编译期保证内存安全。Java 给你内存安全,代价是垃圾回收开销。Python 给你最大的安全和表达力,但执行慢 100 倍。
表达力对简洁:Haskell 的类型系统能表达非常精细的约束,但学习曲线陡峭。Go 刻意省去了泛型(直到最近)、继承和异常,以换取简洁。Python"应该有一种显而易见的方式来做这件事"的哲学让语言容易上手。
控制力对抽象:C/C++ 让你掌控内存布局、缓存行为和硬件交互。Python 把这些都藏起来了。对 ML 训练(GPU 计算占主导)而言,Python 的开销可以忽略;对 ML 推理(每一微秒都很要紧)而言,可能必须用 C++ 或 Rust。
编译速度对运行速度:Go 几秒就编译完(简单的类型系统、极少的优化)。Rust 要编几分钟(复杂的类型系统、激进的优化)。这种权衡是开发者迭代速度对部署性能。
ML 生态正反映了这些权衡:用 Python 做实验和训练(表达力胜出),用 C++/CUDA 做内核和推理(性能胜出),用 Rust 做基础设施和安全攸关系统(安全胜出)。
def make_multiplier(factor): """Returns a function that multiplies by factor.""" def multiply(x): return x * factor return multiply double = make_multiplier(2) triple = make_multiplier(3) print(f"double(5) = {double(5)}") # 10 print(f"triple(5) = {triple(5)}") # 15 # 闭包按引用捕获,而不是按值捕获 def make_counter(): count = [0] # 用可变容器以便修改 def increment(): count[0] += 1 return count[0] return increment counter = make_counter() print(f"counter() = {counter()}") # 1 print(f"counter() = {counter()}") # 2 print(f"counter() = {counter()}") # 3
def add(a, b): return a + b # 适用于不同类型——灵活! print(add(3, 5)) # 8 (int + int) print(add("hello ", "world")) # "hello world" (str + str) print(add([1, 2], [3, 4])) # [1, 2, 3, 4] (list + list) # 但类型错误只在运行时才暴露: try: print(add("hello", 5)) # TypeError! str + int except TypeError as e: print(f"Runtime error: {e}") print("A static type checker would catch this before running")
import time import jax import jax.numpy as jnp n = 1_000_000 # 纯 Python 循环(解释执行) start = time.time() total = 0.0 for i in range(n): total += i * i python_time = time.time() - start # JAX(通过 XLA 编译) @jax.jit def sum_squares_jax(n): return jnp.sum(jnp.arange(n, dtype=jnp.float32) ** 2) _ = sum_squares_jax(10) # 预热 JIT start = time.time() result = sum_squares_jax(n) jax_time = time.time() - start print(f"Python loop: {python_time:.4f}s") print(f"JAX (JIT): {jax_time:.6f}s") print(f"Speedup: {python_time / jax_time:.0f}x")