Files
maths-cs-ai-compendium-zh/chapter 13: computing and OS/02. computer architecture.md
T
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

234 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 计算机体系结构
*计算机体系结构是关于如何构建执行指令的机器。本文涵盖数制、逻辑门、CPU设计、指令集架构、流水线、存储器层次结构和虚拟内存——每个程序、框架和AI模型最终运行其上的硬件基础。*
- 每个神经网络、每个训练循环、每次推理调用最终都会变成流经晶体管的电信号序列。对于严肃的机器学习从业者来说,理解硬件不是可选的:它解释了为什么矩阵乘法很快,为什么内存是瓶颈,为什么GPU主导AI训练,以及为什么缓存友好的代码可以比朴素代码快100倍。
## 数制
- 计算机将所有内容表示为**二进制**(基2):0和1的序列。每个数字是一个**比特**。8个比特为一组称为一个**字节**。二进制数 $b_{n-1} b_{n-2} \ldots b_1 b_0$ 的值为 $\sum_{i=0}^{n-1} b_i \cdot 2^i$。
- 例如,$1011_2 = 1 \cdot 8 + 0 \cdot 4 + 1 \cdot 2 + 1 \cdot 1 = 11_{10}$。
- **十六进制**(基16)是二进制的紧凑表示法。每个十六进制数字代表4个比特:$0\text{-}9$ 映射到 $0000\text{-}1001$$A\text{-}F$ 映射到 $1010\text{-}1111$。因此 $\text{0xFF} = 1111\,1111_2 = 255_{10}$。内存地址和颜色代码通常用十六进制书写。
- **补码**表示有符号整数。对于$n$位数字,最高有效位的权重为 $-2^{n-1}$ 而非 $+2^{n-1}$。8位补码的范围为 $-128$ 到 $+127$。要取一个数的相反数:翻转所有位然后加1。这种表示使加法和减法使用相同的硬件电路,这就是它被普遍采用的原因。
- **IEEE 754浮点数**将实数表示为 $(-1)^s \times 1.m \times 2^{e-\text{bias}}$,其中$s$是符号位,$m$是尾数(小数部分),$e$是移码指数。
![IEEE 754 float32布局:1个符号位、8个指数位、23个尾数位](../images/ieee754_float.svg)
- **float32**(单精度):1个符号 + 8个指数 + 23个尾数 = 32位。范围:$\approx \pm 3.4 \times 10^{38}$,精度:$\approx 7$位十进制数字。
- **float64**(双精度):1个符号 + 11个指数 + 52个尾数 = 64位。范围:$\approx \pm 1.8 \times 10^{308}$,精度:$\approx 15$位十进制数字。
- **float16**(半精度):1 + 5 + 10 = 16位。范围和精度有限,但使用一半的内存和带宽。广泛用于ML训练(混合精度,第6章)。
- **bfloat16**1 + 8 + 7 = 16位。与float32相同的指数范围但精度更低。由Google专门为ML设计:完整的指数范围可防止训练期间溢出,降低的精度对梯度更新是可以接受的。
- 浮点算术**不精确**。在float64中,$0.1 + 0.2 \neq 0.3$(它等于 $0.30000000000000004$)。这是因为$0.1$没有精确的二进制表示,就像$1/3$没有精确的十进制表示一样。在数百万次操作(如梯度下降)中积累这些误差可能导致数值不稳定,这就是为什么存在像损失缩放(第6章)和Kahan求和法这样的技术。
## 逻辑门
- 所有计算都可以归结为**逻辑门**:实现布尔运算(来自文件1的命题逻辑)的物理电路。
- 基本门:
- **与门**(AND):仅当两个输入都为1时输出为1。
- **或门**(OR):至少一个输入为1时输出为1。
- **非门**(NOT,反相器):翻转输入。
- **与非门**NAND,NOT-AND):通用门。任何其他门都可以仅由与非门构建。这就是为什么与非门是数字电路的基本构建块。
- **异或门**(XOR,异或):输入不同时输出为1。对于加法(二进制加法的和位就是XOR)和加密至关重要。
- **半加器**使用XOR(和)和AND(进位)相加两个单比特。**全加器**相加两个比特加上一个进位输入,可以串联起来创建$n$位加法器。这就是CPU执行整数加法的方式:一系列简单逻辑门的级联。
- **多路选择器**(MUX)根据控制信号从多个输入中选择一个。使用$n$个控制位,可以从$2^n$个输入中选择。多路选择器是if-else链的硬件等价物,广泛用于CPU数据通路中路由数据。
- 现代处理器包含数十亿个晶体管,每个晶体管充当一个微小的开关。晶体管要么导通(导电,表示1),要么不导通(不导电,表示0)。门由晶体管构成,加法器由门构成,ALU由加法器构成,CPU由ALU构成。整个计算层级就建立在这个基础之上。
## CPU架构
- **中央处理器(CPU)**执行指令。其核心组件:
- **ALU**(算术逻辑单元):执行整数算术(加、减、乘)和逻辑运算(AND、OR、XOR、移位)。这里是实际计算发生的地方,由上述逻辑门构建而成。
- **寄存器**:CPU内部微小、超快的存储位置。现代CPU有数十个通用寄存器,每个寄存器保存一个字(在64位CPU上为64位)。寄存器是系统中速度最快的存储器:访问时间约~0.3纳秒。
- **程序计数器(PC)**:保存下一条要执行指令的内存地址。
- **控制单元**:解码指令并编排数据通路,告诉ALU执行什么操作以及使用哪些寄存器。
- **指令周期**(取指-译码-执行)每秒重复数十亿次:
1. **取指**:从PC中的地址读取指令。
2. **译码**:确定指令的功能(加法?从内存加载?分支?)及其使用的操作数。
3. **执行**:执行操作(ALU计算、内存访问或分支)。
4. 增加PC(除非指令是分支/跳转)。
- 运行在4 GHz的CPU每秒执行40亿个周期。每个周期耗时0.25纳秒。在这段时间内,光传播约7.5厘米,这就是芯片物理大小重要的原因:信号无法在一个周期内穿过大芯片。
## 指令集架构
- **指令集架构(ISA)**是硬件和软件之间的契约:它定义了CPU能理解的指令、寄存器集、内存模型和编码格式。
- **CISC**(复杂指令集计算机):指令可以复杂、变长,并可以直接访问内存。一条指令可以乘法两个内存值并存储结果。**x86**(Intel/AMD)是占主导地位的CISC ISA,驱动着大多数桌面和服务器。其向后兼容性(现代x86 CPU仍然运行1980年代的代码)既是优势也是负担。
- **RISC**(精简指令集计算机):指令简单、定长,且仅操作寄存器。内存访问需要单独的加载/存储指令。更简单的指令可实现更快的时钟速度和更易实现的流水线。
- **ARM**:移动设备的主要RISC ISA,并越来越多地用于服务器和笔记本电脑(Apple M系列芯片就是ARM)。ARM的能效使其非常适合电池供电和热受限设备。
- **RISC-V**:一个开源的RISC ISA。任何人都可以设计RISC-V芯片而无需许可费。在嵌入式系统、研究和AI加速器中的采用正在增长。
- CISC与RISC的区别已经模糊:现代x86 CPU内部将复杂的CISC指令解码为更简单的微操作(本质上是内部RISC),从而获得两方面的优势。
## 流水线
- 没有流水线时,CPU完全完成一条指令后才开始下一条。这会浪费硬件:当ALU执行时,取指和译码单元处于空闲状态。
![CPU流水线:指令在取指、译码、执行、访存和写回阶段重叠](../images/cpu_pipeline.svg)
- **流水线**使指令执行重叠,如同装配线。当指令1在执行时,指令2在译码,指令3在被取指。一个5级流水线(取指、译码、执行、访存、写回)可以同时有5条指令在执行中。
- 吞吐量接近每周期一条指令(尽管每条指令需要5个周期才能完成)。这与ML中的流水线原理相同:数据并行性使计算和通信重叠(第6章)。
- **冒险**是流水线被破坏的情况:
- **数据冒险**:指令2需要指令1尚未产生的结果。"Add R1, R2, R3"后跟"Sub R4, R1, R5"——第二条指令需要R1,而第一条指令仍在计算。**转发**(旁路)通过将结果直接从一级流水线路由到另一级,无需等待写回阶段来解决这个问题。
- **控制冒险**:分支指令(if-else)意味着CPU在分支解析之前不知道应该取指哪条下一条指令。**分支预测**猜测分支将走哪条路径,并推测性地沿预测路径取指。现代预测器准确率超过95%,使用历史表和类似神经网络的模式匹配。一次预测错误代价约~15个周期(流水线必须被清空并重启)。
- **结构冒险**:两条指令同时需要相同的硬件资源(例如,都需要内存端口)。通过复制资源或插入停顿来解决。
## 存储器层次结构
- 计算机内存中的根本矛盾:快速内存昂贵且容量小,廉价内存缓慢但容量大。**存储器层次结构**通过利用**局部性**来弥合这一差距:程序倾向于重复访问相同的数据(时间局部性)并访问附近的数据(空间局部性)。
![存储器层次结构金字塔:寄存器在顶部(快速、小)到HDD在底部(慢、大)](../images/memory_hierarchy.svg)
- 层次结构,从最快到最慢:
- **寄存器**:~0.3 ns访问,总容量~KB。位于CPU内。
- **L1缓存**~1 ns,每核心32-64 KB。分为指令缓存和数据缓存。
- **L2缓存**~4 ns,每核心256 KB-1 MB。
- **L3缓存**~10 ns,跨核心共享8-64 MB。
- **RAMDRAM**~50-100 ns8-512 GB。主内存。
- **SSD**~10-100 μs256 GB-8 TB。持久存储。
- **HDD**~5-10 ms1-20 TB。机械式,随机访问非常慢。
- 寄存器和RAM之间的速度差距约为300倍。寄存器和磁盘之间约为30,000,000倍。缓存层次结构隐藏了这一差距:如果CPU需要的数据在L1缓存中(**缓存命中**),访问很快。如果不在(**缓存未命中**),CPU停顿,同时从更慢的层级获取数据。
- **缓存关联度**决定内存地址可以存储在缓存中的位置:
- **直接映射**:每个地址映射到恰好一个缓存行。简单但会导致冲突。
- **全关联**:任何地址可以放在任何位置。灵活但搜索成本高。
- **组关联**($k$路):每个地址映射到一组$k$个位置。实际CPU中使用的实用折衷方案(通常为4路或8路)。
- **缓存一致性**确保所有CPU核心看到一致的内存视图。当核心1写入一个核心2已缓存的内存地址时,一致性协议(如MESI)会使核心2的副本失效或更新。这对并发编程(文件4)至关重要,也是共享内存并行性困难的原因之一。
- 对于ML从业者,存储器层次结构解释了为什么:
- 矩阵运算应按顺序访问内存(行优先与列优先的布局很重要)。
- 批量大小会影响性能:更大的批次分摊内存延迟。
- 混合精度(float16/bfloat16)使有效内存带宽翻倍,而内存带宽往往是瓶颈。
## 虚拟内存
- **虚拟内存**使每个进程仿佛拥有自己独立、连续的大内存空间,即使物理RAM是有限的并在进程间共享。
- 地址空间被划分为固定大小的**页**(通常为4 KB)。**页表**将虚拟页号映射到物理帧号。当程序访问虚拟地址0x1234时,CPU通过查找页表将其转换为物理地址。
- **转译后备缓冲器(TLB)**是页表项的缓存。由于页表位于RAM中(慢速),TLB在快速硬件中存储最近使用的转译结果。TLB未命中需要遍历内存中的页表,耗费数百个周期。
- 当程序访问一个不在物理RAM中的页时,发生**缺页**。OS从磁盘加载该页(交换),耗费数百万个周期。过多的缺页(**系统颠簸**)会严重损害性能。这就是为什么ML训练需要足够的RAM来容纳模型、优化器状态和合理的数据批次。
- **页面置换**算法决定当RAM满时应换出哪个页面:
- **LRU**(最近最少使用):换出最长时间未被访问的页面。在实践中对大多数工作负载最优。在硬件中通过**时钟算法**(带引用位的循环链表)近似实现。
- **FIFO**:换出最旧的页面。简单但可能换出频繁使用的页面。
- **最优**(Bélády算法):换出将在最长时间内不被使用的页面。无法实现(需要未来知识)但可作为理论基准。
- 虚拟内存还提供了**隔离**:每个进程都有自己的虚拟地址空间。一个进程中的错误不会破坏另一个进程的内存,因为它们的虚拟地址映射到不同的物理帧。这是OS安全性和稳定性的基础。
## I/O、中断和DMA
- CPU需要与外部世界通信:磁盘、网卡、键盘、GPU。这就是**I/O子系统**。
- **程序控制I/O**(轮询):CPU在一个循环中反复检查设备的状态寄存器,等待数据就绪。简单但浪费CPU周期做空转而不是有用工作。
- **中断驱动I/O**:设备在数据就绪时发送一个硬件**中断**。CPU继续正常执行直到中断到达,然后运行一个**中断处理程序**(内核函数)来处理数据。这比轮询高效得多,因为CPU在等待时不会空闲。
- 中断机制:
1. 设备通过硬件线路发出中断信号。
2. CPU完成当前指令,将当前状态(寄存器、程序计数器)保存到堆栈。
3. CPU在**中断向量表**(每个中断类型对应一个函数指针的表)中查找中断处理程序地址。
4. 处理程序在内核模式下运行,处理I/O,然后返回。
5. CPU恢复保存的状态并恢复被中断的程序。
- 这与上下文切换(文件3)的保存/恢复模式相同,但由硬件而非定时器触发。
- **DMA**(直接存储器访问):对于大数据传输(磁盘读取、网络数据包、GPU内存复制),让CPU逐字节复制数据是浪费的。**DMA控制器**直接在设备和RAM之间传输数据,无需CPU参与。CPU设置传输(源地址、目标地址、大小),DMA控制器处理传输,完成后CPU收到一个中断。
- DMA对ML至关重要:当你调用 `model.to('cuda')` 时,数据通过PCIe总线上的DMA从系统RAM传输到GPU内存。在训练期间,跨GPU的梯度同步使用基于DMA的RDMA(远程DMA)进行高带宽、低延迟传输(第6章)。
- **总线**将CPU连接到内存和I/O设备。现代系统使用**PCIe**(快速外设组件互连)连接高速设备(GPU、NVMe SSD、网卡)。PCIe 4.0在每个x16插槽上提供约~32 GB/s;PCIe 5.0将其翻倍。总线带宽通常是GPU训练的瓶颈:GPU的计算速度可能快于数据送达的速度。
- **MMIO**(内存映射I/O):设备寄存器被映射到内存地址。CPU使用普通的加载/存储指令对这些地址进行读写,硬件将访问路由到设备而不是RAM。这统一了内存和I/O访问为一个单一机制,简化了硬件和软件。
## 编程任务(使用CoLab或笔记本)
1. 探索IEEE 754浮点数表示。将浮点数转换为二进制表示,观察符号、指数和尾数字段。
```python
import struct
def float_to_bits(f):
"""显示float32的IEEE 754二进制表示。"""
packed = struct.pack('>f', f)
bits = ''.join(f'{byte:08b}' for byte in packed)
sign = bits[0]
exponent = bits[1:9]
mantissa = bits[9:]
return sign, exponent, mantissa
for val in [1.0, -1.0, 0.1, 0.5, 3.14, float('inf'), float('nan')]:
s, e, m = float_to_bits(val)
print(f"{val:>10} sign={s} exp={e} ({int(e, 2) - 127:>4d}) mantissa={m[:10]}...")
```
2. 模拟直接映射缓存。跟踪一系列内存访问的命中与未命中。
```python
def simulate_cache(accesses, cache_size=8, block_size=1):
"""模拟直接映射缓存。"""
cache = [None] * cache_size
hits, misses = 0, 0
for addr in accesses:
cache_line = addr % cache_size
if cache[cache_line] == addr:
hits += 1
status = "HIT "
else:
misses += 1
cache[cache_line] = addr
status = "MISS"
print(f" Access {addr:3d} → line {cache_line}: {status}")
print(f"\nHits: {hits}, Misses: {misses}, Hit rate: {hits/(hits+misses):.1%}")
# 顺序访问(良好的局部性)
print("顺序访问:")
simulate_cache([0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3])
# 跨步访问(冲突未命中)
print("\n跨步访问(stride = cache size):")
simulate_cache([0, 8, 0, 8, 0, 8])
```
3. 演示为什么浮点算术不满足结合律。展示 $(a + b) + c \neq a + (b + c)$ 的情况。
```python
import jax.numpy as jnp
a = jnp.float32(1e8)
b = jnp.float32(1.0)
c = jnp.float32(-1e8)
left = (a + b) + c # (1e8 + 1) + (-1e8)
right = a + (b + c) # 1e8 + (1 + (-1e8))
print(f"(a + b) + c = {left}") # 应为 1.0
print(f"a + (b + c) = {right}") # 可能会丢失 1.0
print(f"Equal: {left == right}")
print(f"\n当 1.0 加到 1e8 上时被丢失,因为 float32 只有约 7 位精度")
```