跳转至

# 编程语言

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

- 每一份软件、每一个ML模型、每一个操作系统都是用编程语言编写的。但存在数百种语言，每种都有不同的优势。为什么？因为语言设计涉及基本的权衡：性能 vs 安全、表现力 vs 简洁性、控制 vs 抽象。理解这些权衡有助于你为工作选择合适的工具，并理解你所处的约束。

## 语言范式

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

- 命令式编程将计算描述为一系列改变状态的命令。"设x为5。将3加到x。如果x > 7，打印它。"C、Python和Java本质上是命令式的。心智模型是一个带有内存的机器，你逐步修改它。

- 面向对象（OOP）编程围绕对象组织代码：数据（属性）和行为（方法）的捆绑。对象通过相互发送消息来交互。关键思想是封装（将内部状态隐藏在公共接口之后）、继承（通过扩展现有类创建新类）和多态（通过共享接口统一处理不同类型）。Java、C++和Python支持OOP。

- 函数式编程（FP）将计算视为数学函数的求值。核心原则：不可变性（数据一旦创建就不改变）、纯函数（输出仅取决于输入，无副作用）和一等函数（函数是可以作为参数传递、从其他函数返回和存储在变量中的值）。Haskell是纯函数式的。Python、JavaScript和Scala支持函数式风格。

- 纯函数易于推理、测试和并行化（没有共享的可变状态意味着没有竞态条件）。这就是为什么函数式思想越来越多地用于分布式系统和数据管道。JAX（本书中一直在使用）是函数式的：`jax.grad` 之所以有效，是因为JAX函数是纯函数。

- 逻辑编程描述什么应该为真，而不是如何计算它。你陈述事实和规则，运行时找到解。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是弱类型的：你可以将指针强制转换为整数。

- 类型推断让编译器推导类型而无需显式注解：

    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的动态类型使实验快速，但隐藏了错误。生产ML系统越来越多地使用类型提示（`def train(model: nn.Module, lr: float) -> float`）和静态分析工具（mypy）以在部署前捕获错误。PyTorch和JAX使用Python以获得灵活性；TensorRT和ONNX Runtime使用C++以获得性能。

## 内存管理

- 每个程序分配和释放内存。如何管理这是最具影响力的语言设计决策之一。

- 堆栈存储局部变量和函数调用帧。分配很简单（移动栈指针），释放是自动的（函数返回时弹出帧）。堆栈访问很快，因为它总在缓存中。但堆栈有固定大小（通常1-8 MB），且仅支持LIFO（后进先出）分配。

- 堆存储动态分配的数据（编译时大小未知的对象、数组、字符串）。堆分配较慢（需要找到一个空闲块），需要显式或自动释放。堆可以增长到填满可用内存。

- 手动内存管理（C、C++）：程序员显式分配（`malloc`）和释放（`free`）堆内存。最大控制和性能，但极易出错：

  - 释放后使用：访问已被释放的内存。导致崩溃或安全漏洞。
  - 双重释放：释放同一内存两次。破坏分配器的内部数据结构。
  - 内存泄漏：分配了内存但从未释放。程序慢慢消耗所有可用RAM。

- 垃圾回收（GC）：运行时自动检测并释放不再可达的内存。程序员从不调用 `free`。

  - 跟踪GC（Java、Go、Python的循环收集器）：定期从"根"（堆栈变量、全局变量）遍历所有可达对象，释放不可达对象。简单但导致GC暂停：收集器运行时程序停止。现代收集器（Go的并发GC、Java的ZGC）将暂停时间最小化到亚毫秒级。

  - 引用计数（Python的主要机制、Swift、Objective-C）：每个对象跟踪有多少引用指向它。当计数降到0时，对象被立即释放。无暂停，但无法处理循环（A引用B，B引用A，两者计数都 > 0 但都不可达）。Python使用单独的循环检测器来处理此问题。

- 所有权（Rust）：编译器在编译时强制实施内存安全规则，零运行时开销。

  - 每个值有且仅有一个所有者。当所有者超出作用域时，该值被丢弃（释放）。
  - 值可以被借用（引用），但编译器强制：要么一个可变引用，要么任意数量的不可变引用，永远不能同时存在。
  - 这阻止了释放后使用、双重释放、数据竞争和悬垂指针，全部在编译时完成。无需GC，无运行时开销。

- 借用检查器是Rust的杀手级特性，也是其最陡峭的学习曲线。它保证了内存安全和线程安全，且没有垃圾回收，这就是Rust越来越多地用于性能关键系统（OS内核、游戏引擎、ML推理运行时如Candle和Burn）的原因。

## 编译流水线

- 编译器在程序运行之前将源代码转换为机器码（或其他目标语言）。该流水线有几个阶段：

1. 词法分析（分词）：将源文本转换为令牌流。`x = 3 + y` 变为 `[IDENT("x"), EQUALS, INT(3), PLUS, IDENT("y")]`。词法分析器去除空白和注释。

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

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

4. 优化：在不改变行为的情况下转换程序以使其运行更快。常见优化：

   - 常量折叠：在编译时计算 `3 + 5`，替换为 `8`。
   - 死代码消除：移除永远无法执行的代码。
   - 循环展开：用重复的内联代码替换循环以减少分支开销。
   - 内联：用函数体替换函数调用，消除调用开销。

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慢（解释开销），但对于长时间运行的程序（JIT优化的热路径）接近C的速度。

- JIT（即时）编译在运行时将代码编译为机器码，使用仅在执行期间可用的信息。JIT可以根据实际运行时数据进行优化：如果一个函数总是用整数参数调用，JIT生成专门化的仅整数机器码，跳过类型检查。

- PyPy是另一个带有JIT编译器的Python实现。它通过将热点循环JIT编译为机器码，使大多数Python代码运行速度比CPython快5-10倍。然而，它与C扩展模块（NumPy、PyTorch）的兼容性有限，这限制了它在ML中的使用。

- 从解释到编译的范围不是二元的：

  - 纯解释：Bash shell脚本。
  - 字节码解释：CPython。
  - 字节码 + JIT：JVM、.NET CLR、LuaJIT、PyPy。
  - 提前（AOT）编译：C、C++、Rust、Go。
  - AOT + 运行时代码生成：JAX的 `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

- 闭包是回调、装饰器和部分应用背后的机制。它们对函数式编程至关重要。

- 模式匹配：一种强大的控制流机制，解构数据并根据其形状进行分支：

    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`）。

- 代数数据类型（ADT）：可以是多个变体之一的类型，每个变体携带不同的数据。`Result` 类型要么是 `Ok(value)` 要么是 `Err(error)`。`Tree` 要么是 `Leaf(value)` 要么是 `Node(left, right)`。ADT结合模式匹配可以穷尽处理所有情况，消除整类bug（空指针异常、未处理的错误码）。

- 特质与接口：定义一个类型必须实现的一组方法，而不指定如何实现。这实现了多态：一个接受"任何实现了Display特质的类型"的函数可以处理整数、字符串和自定义类型。Rust使用特质，Java使用接口，Go使用隐式接口，Python使用鸭子类型（"如果它走路像鸭子……"）。

## 领域特定语言

- 领域特定语言（DSL）是为特定问题域设计的语言，在该领域内用通用性换取表现力。

- SQL：关系数据库的语言。`SELECT name FROM users WHERE age > 30` 比等价的命令式循环可读性强得多且更易优化。数据库引擎优化查询执行计划，自动选择连接策略和索引使用。

- 正则表达式：用于文本模式匹配的微型语言。`\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++让你控制内存布局、缓存行为和硬件交互。Python隐藏了所有这些。对于ML训练（GPU计算占主导），Python的开销可以忽略不计。对于ML推理（每微秒都很关键），C++或Rust可能是必要的。

- 编译速度 vs 运行时速度：Go在几秒内编译完成（简单的类型系统，最小优化）。Rust需要几分钟编译（复杂的类型系统，激进优化）。权衡的是开发者迭代速度与部署后的性能。

- ML生态系统反映了这些权衡：Python用于实验和训练（表现力取胜），C++/CUDA用于内核和推理（性能取胜），Rust用于基础设施和安全关键系统（安全取胜）。

## 编程任务（使用CoLab或笔记本）

1. 探索闭包和高阶函数。实现一个简单的函数工厂，验证闭包捕获其环境。

       def make_multiplier(factor):
           """返回一个将输入乘以 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

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 loop: {python_time:.4f}s")
       print(f"JAX (JIT):   {jax_time:.6f}s")
       print(f"Speedup:     {python_time / jax_time:.0f}x")