# 编程语言 *编程语言是人类意图与机器执行之间的接口。本文涵盖语言范式、类型系统、内存管理策略、编译流水线、解释与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是静态类型的。你必须声明类型(或者编译器推断它们): ```rust let x: i32 = 5; // Rust:x是一个32位整数 let y: f64 = 3.14; // y是一个64位浮点数 // let z = x + y; // 编译错误:不能加 i32 和 f64 ``` - **动态类型**:类型在**运行时**检查,当操作实际执行时。更灵活,但类型错误只有在代码运行时才暴露。Python、JavaScript和Ruby是动态类型的: ```python x = 5 # x是一个int(目前) x = "hello" # 现在x是一个字符串——没有错误 ``` - **强类型**:语言阻止隐式类型转换。Python是强类型的:`"3" + 5` 引发TypeError。**弱类型**:语言静默地转换类型。JavaScript是弱类型的:`"3" + 5` 得到 `"35"`(数字被强制转换为字符串)。C是弱类型的:你可以将指针强制转换为整数。 - **类型推断**让编译器推导类型而无需显式注解: ```rust let x = 5; // 编译器推断:i32 let y = x + 3.0; // 编译错误:混合类型,即使有推断 ``` - **泛型**(参数化多态)让你编写适用于任何类型的代码: ```rust fn largest(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++以获得性能。 ## 内存管理 - 每个程序分配和释放内存。如何管理这是最具影响力的语言设计决策之一。 ![内存布局:堆栈从高地址向下增长,堆向上增长,代码和数据在底部](../images/stack_vs_heap.svg) - **堆栈**存储局部变量和函数调用帧。分配很简单(移动栈指针),释放是自动的(函数返回时弹出帧)。堆栈访问很快,因为它总在缓存中。但堆栈有固定大小(通常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)的原因。 ## 编译流水线 - **编译器**在程序运行之前将源代码转换为机器码(或其他目标语言)。该流水线有几个阶段: ![编译流水线:源代码→词法分析器→解析器→语义分析→优化器→代码生成→机器码](../images/compilation_pipeline.svg) 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)或中间表示。 - **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代码,然后缓存编译后的版本。 ## 关键语言特性 - **闭包**:捕获其包围作用域中变量的函数。该函数"闭合"其定义时的环境: ```python def make_adder(n): def add(x): return x + n # n 从包围作用域捕获 return add add5 = make_adder(5) print(add5(3)) # 8 ``` - 闭包是回调、装饰器和部分应用背后的机制。它们对函数式编程至关重要。 - **模式匹配**:一种强大的控制流机制,解构数据并根据其形状进行分支: ```rust 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. 探索闭包和高阶函数。实现一个简单的函数工厂,验证闭包捕获其环境。 ```python 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。 ```python 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方法在计算密集型任务上的性能差异。 ```python 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") ```