Skip to content

编程语言

编程语言是人类意图与机器执行之间的接口。本文涵盖语言范式、类型系统、内存管理策略、编译流水线、解释执行与 JIT 编译、关键语言特性、领域特定语言以及设计权衡。

  • 每一段软件、每一个 ML 模型、每一个操作系统,都是用某种编程语言编写的。但编程语言有数百种,各有不同的优势。为什么?因为语言设计涉及根本性的权衡:性能与安全、表达力与简洁、控制与抽象。理解这些权衡有助于你为工作选择合适的工具,并理解你所处的约束边界。

语言范式

  • 范式(paradigm)是一种编程风格:一套指导你如何组织代码和思考问题的原则。

  • 命令式(Imperative)编程将计算描述为一系列改变状态的命令。"把 x 赋值为 5。给 x 加 3。如果 x > 7,打印它。"C、Python 和 Java 在本质上都是命令式的。其思维模型是一台带有内存的机器,你逐步修改它的状态。

  • 面向对象(OOP)编程围绕对象组织代码:将数据(属性)和行为(方法)捆绑在一起的单元。对象通过互相发送消息来交互。核心思想包括:封装(将内部状态隐藏在公共接口后面)、继承(通过扩展已有的 class 创建新 class)以及多态(通过共享接口统一处理不同类型)。Java、C++ 和 Python 都支持 OOP。

  • 函数式编程(FP)将计算视为数学 function 的求值。核心原则:不可变性(数据一旦创建就不改变)、纯 function(输出只依赖于输入,无副作用)以及一等 function(function 是值,可作为参数传递、从其他 function 返回,并存储在变量中)。Haskell 是纯函数式语言。Python、JavaScript 和 Scala 都支持函数式风格。

  • 纯 function 易于推理、测试和并行化(没有共享可变状态意味着没有竞态条件)。这就是为什么函数式思想越来越多地被用于分布式系统和数据 pipeline。本书中大量使用的 JAX 是函数式的:jax.grad 能够工作,正是因为 JAX 的 function 是纯的。

  • 逻辑编程描述什么应该为真,而不是如何计算。你陈述事实和规则,运行时负责找出解答。Prolog 是经典示例:给定"苏格拉底是人"和"所有人都是凡人",引擎可以推导出"苏格拉底是凡人"。逻辑编程用于 AI 知识库和类型检查。

  • 大多数现代语言是多范式的:Python 支持命令式、OOP 和函数式风格。Rust 支持命令式和函数式。范式是工具,不是信仰。

类型系统

  • 类型对值进行分类,并决定哪些操作合法。整数 3 和字符串 "3" 是不同的类型:整数可以相加,字符串不能(字符串可以拼接,但那是不同的操作)。

  • 静态类型:类型在编译期检查,即程序运行之前。类型错误会被提前发现。C、Java、Rust 和 Go 是静态类型的。你必须声明类型(或让编译器推断):

let x: i32 = 5;     // Rust:x 是 32 位整数
let y: f64 = 3.14;  // y 是 64 位浮点数
// let z = x + y;    // 编译错误:不能将 i32 与 f64 相加
  • 动态类型:类型在运行时检查,即操作真正执行时。更灵活,但类型错误只有在代码运行时才会暴露。Python、JavaScript 和 Ruby 是动态类型的:
x = 5       # x 是 int(现在)
x = "hello" # 现在 x 是字符串 —— 没有报错
  • 强类型:语言阻止隐式类型转换。Python 是强类型的:"3" + 5 会抛出 TypeError。弱类型:语言静默地进行类型转换。JavaScript 是弱类型的:"3" + 5 得到 "35"(数字被强制转换为字符串)。C 是弱类型的:你可以将 pointer 强制转换为整数。

  • 类型推断让编译器在不需要显式注解的情况下推断类型:

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
}
// 适用于整数、浮点数、字符串 —— 任何支持比较的类型
  • 对于 ML:Python 的动态类型使实验快速,但会隐藏 bug。生产 ML 系统越来越多地使用类型提示(def train(model: nn.Module, lr: float) -> float)和静态分析工具(mypy)在部署前捕获错误。PyTorch 和 JAX 使用 Python 提供灵活性;TensorRT 和 ONNX Runtime 使用 C++ 提供性能。

内存管理

  • 每个程序都需要分配和释放内存。如何管理内存是语言设计中最关键的决策之一。

内存布局:stack 从高地址向下增长,heap 从低地址向上增长,代码和数据段位于底部

  • stack 存储局部变量和 function 调用帧。分配极其简单(移动 stack 指针),释放是自动的(function 返回时弹出帧)。stack 访问速度极快,因为它始终在 cache 中。但 stack 的大小固定(通常为 1-8 MB),且只支持 LIFO(后进先出)分配。

  • heap 存储动态分配的数据(对象、array、编译时大小未知的字符串)。heap 分配较慢(需要找一个空闲块),并且需要显式或自动释放。heap 可以增长以填满可用内存。

  • 手动内存管理(C、C++):程序员显式分配(malloc)并释放(free)heap 内存。最大控制力和性能,但极易出错:

    • Use-after-free:访问已经被释放的内存。导致崩溃或安全漏洞。
    • Double free:两次释放同一块内存。破坏分配器的内部数据结构。
    • 内存泄漏:分配了内存但从不释放。程序慢慢耗尽所有可用 RAM。
  • 垃圾回收(GC):运行时自动检测并释放不再可达的内存。程序员无需调用 free

    • 标记-清除 GC(Java、Go、Python 的循环收集器):周期性地从"根"(stack 变量、全局变量)遍历所有可达对象,并释放不可达对象。简单,但会导致 GC 暂停:垃圾回收器运行期间程序停止。现代收集器(Go 的并发 GC、Java 的 ZGC)将暂停时间缩短至毫秒以下。

    • 引用计数(Python 的主要机制、Swift、Objective-C):每个对象跟踪有多少引用指向它。当计数降为 0 时,对象立即被释放。没有暂停,但无法处理循环引用(A 引用 B,B 引用 A,两者计数都 > 0,但两者都不可达)。Python 使用单独的循环检测器来处理这种情况。

  • 所有权(Rust):编译器在编译期以零运行时开销强制执行内存安全规则。

    • 每个值只有一个所有者。当所有者离开作用域时,值被销毁(释放)。
    • 值可以被借用(引用),但编译器强制执行:要么只有一个可变引用,要么有任意数量的不可变引用,两者不能同时存在。
    • 这在编译期防止了 use-after-free、double free、数据竞争和悬空 pointer,全无运行时开销,也不需要 GC。
  • 借用检查器(borrow checker)是 Rust 的杀手级特性,也是其最陡峭的学习曲线。它在无垃圾回收的情况下保证内存安全和线程安全,这就是为什么 Rust 越来越多地用于性能关键型系统(OS kernel、游戏引擎、ML 推理运行时如 Candle 和 Burn)。

编译流水线

  • 编译器在程序运行之前将源代码翻译成机器码(或其他目标语言)。编译流水线包含几个阶段:

编译流水线:源代码 → 词法分析器 → 语法分析器 → 语义分析 → 优化器 → 代码生成 → 机器码

  1. 词法分析(tokenization):将源代码文本转换为 token 流。x = 3 + y 变为 [IDENT("x"), EQUALS, INT(3), PLUS, IDENT("y")]。词法分析器去除空白和注释。

  2. 语法分析(parsing):从 token 流构建抽象语法树(AST)。AST 表示程序的层次结构。3 + y * 2 被解析为 Add(3, Mul(y, 2))(乘法具有更高的优先级)。语法分析器检查语法:括号不匹配和缺少分号在此处被捕获。

  3. 语义分析:检查类型、解析变量名、验证 function 是否以正确的参数调用。静态类型检查就发生在这里。输出是带类型注解的 AST。

  4. 优化:在不改变程序行为的前提下,将程序转换为运行更快的形式。常见优化:

    • 常量折叠:在编译期计算 3 + 5,将其替换为 8
    • 死代码消除:删除永远不会执行的代码。
    • 循环展开:用重复的内联代码替换循环,减少分支开销。
    • 内联:用 function 的函数体替换 function 调用,消除调用开销。
  5. 代码生成:将优化后的表示翻译成目标机器码(x86、ARM)或中间表示。

  6. LLVM 是主流的编译器基础设施。它提供一种公共中间表示(LLVM IR),许多语言都编译到这种表示。LLVM 的优化器作用于 IR,其后端为多种目标生成机器码。Clang(C/C++)、Rust、Swift、Julia 以及许多其他语言都使用 LLVM。这意味着对 LLVM 优化器的改进可以同时惠及所有这些语言。

解释执行与 JIT 编译

  • 解释器逐行(或逐语句)执行程序,而不生成机器码。这使启动速度快、开发交互性好,但执行较慢(每一行每次运行时都要重新分析)。

  • 大多数解释型语言实际上会编译为字节码:一种比源代码简单、但与机器无关的中间表示。字节码在虚拟机(VM)上运行。

    • CPython(标准 Python 实现)将 Python 源代码编译为字节码(.pyc 文件),由 CPython VM 执行。VM 逐条解释字节码。这就是为什么 Python 在计算密集型代码上比 C 慢约 100 倍。

    • JVM(Java 虚拟机):Java 编译为 JVM 字节码(.class 文件)。JVM 先解释字节码,然后对频繁执行的代码路径("热点")进行 JIT 编译,生成本地机器码。这就是为什么 Java 启动比 C 慢(解释开销),但对于长时间运行的程序,性能接近 C(JIT 优化的热路径)。

  • JIT(即时编译)在运行时将代码编译为机器码,利用只有在执行期间才能获得的信息。JIT 可以根据实际运行时数据进行优化:如果一个 function 总是以整数参数调用,JIT 会生成专门的仅整数机器码,跳过类型检查。

  • PyPy 是带有 JIT 编译器的替代 Python 实现。它通过将热循环 JIT 编译为机器码,使大多数 Python 代码比 CPython 快 5-10 倍。然而,它与 C 扩展 module(NumPy、PyTorch)的兼容性有限,这限制了其在 ML 中的使用。

  • 从解释到编译的谱系并非非此即彼:

    • 纯解释:bash shell 脚本。
    • 字节码解释:CPython。
    • 字节码 + JIT:JVM、.NET CLR、LuaJIT、PyPy。
    • 提前编译(AOT):C、C++、Rust、Go。
    • AOT + 运行时代码生成:JAX 的 jax.jit 在首次调用时将 Python function 编译为优化的 XLA 代码,然后缓存编译后的版本。

关键语言特性

  • 闭包(Closures):捕获其外层作用域变量的 function。function "封闭"了它被定义时所在的环境:
def make_adder(n):
    def add(x):
        return x + n  # n 从外层作用域被捕获
    return add

add5 = make_adder(5)
print(add5(3))  # 8
  • 闭包是回调、装饰器和偏应用背后的机制。它是函数式编程的基础。

  • 模式匹配(Pattern matching):一种强大的控制流机制,对数据进行解构并根据其结构进行分支:

match value {
    Some(x) if x > 0 => println!("正数: {}", x),
    Some(0)           => println!("零"),
    Some(x)           => println!("负数: {}", x),
    None              => println!("空"),
}
  • 模式匹配比 if-else 链更具表达力:它检查数据的结构(是 Some 还是 None?包含满足条件的值吗?),而不仅仅是相等性。Python 在 3.10 版本中添加了结构化模式匹配(match/case)。

  • 代数数据类型(ADTs):可以是几种变体之一的类型,每种变体携带不同的数据。Result 类型要么是 Ok(value) 要么是 Err(error)Tree 要么是 Leaf(value) 要么是 Node(left, right)。ADTs 结合模式匹配可以对所有情况进行穷举处理,消除整类 bug(空指针异常、未处理的错误码)。

  • Trait 与接口:定义一个类型必须实现的方法集合,而无需指定如何实现。这使多态成为可能:接受"任何实现了 Display trait 的类型"的 function,可以处理整数、字符串和自定义类型。Rust 使用 trait,Java 使用 interface,Go 使用隐式 interface,Python 使用鸭子类型("如果它走路像鸭子……")。

领域特定语言

  • 领域特定语言(DSL)是为特定问题领域设计的语言,以通用性换取该领域内的表达力。

  • SQL:关系型数据库的语言。SELECT name FROM users WHERE age > 30 比等价的命令式循环更易读、更易优化。数据库引擎自动优化查询执行计划,选择 join 策略和索引使用方式。

  • 正则表达式:用于文本模式匹配的迷你语言。\d{3}-\d{4} 匹配如 "555-1234" 这样的电话号码。正则表达式引擎将模式编译为有限自动机,以实现高效匹配。

  • 着色器语言(GLSL、HLSL、Metal Shading Language):在 GPU 核心上运行的程序,用于计算像素颜色、顶点位置或计算操作。着色器是大规模并行的:每次调用独立处理一个像素或一个元素。这与 CUDA 用于 ML 计算的执行模型相同。

  • 在 ML 中,PyTorch 和 JAX 等框架本质上是嵌入在 Python 中的张量计算 DSL。它们提供领域特定的抽象(张量、自动微分、设备放置),同时利用 Python 的生态系统。

语言设计权衡

  • 没有哪种语言能在所有方面都是最优的。设计就是选择做出哪些权衡:

  • 性能 vs 安全:C 提供原始速度和硬件控制,但允许内存破坏。Rust 提供相当的速度和编译期内存安全。Java 提供带有垃圾回收开销的内存安全。Python 提供最高的安全性和表达力,但执行速度慢约 100 倍。

  • 表达力 vs 简洁:Haskell 的类型系统可以表达非常精确的约束,但学习曲线陡峭。Go 刻意省略了泛型(直到最近)、继承和异常,以求简洁。Python 的"应该有一种显而易见的方式来做这件事"的哲学保持了语言的可学性。

  • 控制 vs 抽象:C/C++ 让你控制内存布局、cache 行为和硬件交互。Python 隐藏了这一切。对于 ML 训练(GPU 计算占主导),Python 的开销可以忽略不计。对于 ML 推理(每微秒都很重要),C++ 或 Rust 可能是必要的。

  • 编译速度 vs 运行速度:Go 在几秒内完成编译(简单类型系统,最小优化)。Rust 需要几分钟(复杂类型系统,激进优化)。这是开发者迭代速度与部署性能之间的权衡。

  • ML 生态系统反映了这些权衡:Python 用于实验和训练(表达力优先),C++/CUDA 用于 kernel 和推理(性能优先),Rust 用于基础设施和安全关键型系统(安全性优先)。

编程练习(使用 CoLab 或 notebook)

  1. 探索闭包和高阶 function。实现一个简单的 function 工厂,并验证闭包确实捕获了其所在环境。

    def make_multiplier(factor):
        """返回一个乘以 factor 的 function。"""
        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
    

  2. 比较动态类型与静态类型的行为。展示 Python 的动态类型如何带来灵活性,但也可能隐藏 bug。

    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"运行时错误: {e}")
        print("静态类型检查器会在运行前捕获这个错误")
    

  3. 测量解释型 Python 与编译/JIT 方案在计算密集型任务上的性能差异。

    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 循环: {python_time:.4f}s")
    print(f"JAX (JIT):   {jax_time:.6f}s")
    print(f"加速比:      {python_time / jax_time:.0f}x")