计算机组成原理

计算机组成原理

计算机组成原理研究计算机的硬件组成、工作原理与体系结构,是计算机科学的核心基础课,帮助理解"计算机如何工作"。

本教程包含

  • 数据表示:二进制、进制转换、负数/小数/字符表示
  • 电路基础:布尔代数、逻辑门、加法器、触发器与寄存器
  • 整机结构:计算机骨架、程序与数据、指令周期、时钟频率
  • 性能与存储:流水线、多核心、存储金字塔、缓存、内存寻址
  • 输入输出:I/O 方式、指令完整旅程

为什么学习

  • 理解程序如何被执行(编译→汇编→机器码)
  • 写出高性能代码(缓存友好、局部性原理)
  • 为操作系统、编译原理打基础
  • 面试常考点(冯诺依曼、指令周期等)

核心概念

冯·诺依曼体系: 存储程序 + 程序控制
五大部件: 运算器、控制器、存储器、输入、输出
指令 = 操作码 + 操作数

计算机只认识 0 和 1

计算机只认识 0 和 1

计算机的一切数据(数字、文字、图片、声音、视频)在底层都是 0 和 1 的二进制序列。

为什么是二进制

1. 物理实现简单: 电压高低、开关通断、磁场方向
2. 抗干扰强: 只有两种状态,不易误判
3. 逻辑运算简单: 布尔代数只有 0/1
4. 可靠性高、成本低

晶体管开关

晶体管是现代计算机的基本元件
相当于一个"电子开关":
  导通 = 1(高电平)
  截止 = 0(低电平)

数十亿晶体管组合 → CPU/内存/硬盘

二进制应用

数字: 42 = 101010(二进制)
文字: 'A' = 65 = 01000001(ASCII)
图片: 像素颜色 = RGB 数值
声音: 采样值序列
视频: 图像序列 + 压缩数据

为什么不用十进制

十进制需要 10 种稳定状态,物理实现难
二进制只有 2 种,用电压区分即可
多进制会提高单线信息密度但降低可靠性
(计算机内部也有 16 进制,但只是二进制的简写)

位的概念

1 bit(位): 一个 0 或 1
1 Byte(字节) = 8 bit
1 KB = 1024 B   1 MB = 1024 KB
1 GB = 1024 MB  1 TB = 1024 GB

进制转换

进制转换

掌握二进制、八进制、十进制、十六进制之间的转换,是理解计算机数据的基础。

位权概念

任意进制: 每位值 = 数字 × 基数^位权
十进制 123 = 1×10² + 2×10¹ + 3×10⁰
二进制 101 = 1×2² + 0×2¹ + 1×2⁰ = 5

二进制 → 十进制

1010₂ = 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 8 + 0 + 2 + 0 = 10

Python:
int("1010", 2)   # 10

十进制 → 二进制(除2取余)

13 ÷ 2 = 6 余 1  ↑
 6 ÷ 2 = 3 余 0  │
 3 ÷ 2 = 1 余 1  │
 1 ÷ 2 = 0 余 1  │
从下往上读: 1101₂ = 13

bin(13)   # '0b1101'

二进制 ↔ 八进制/十六进制

二进制每3位 = 1位八进制
  101 110 = 56₈
二进制每4位 = 1位十六进制
  1011 1010 = BA₁₆

# Python
oct(90)    # '0o132'
hex(90)    # '0x5a'
int("BA", 16)   # 186

小数转换

0.625 × 2 = 1.25 → 取1, 剩0.25
0.25 × 2 = 0.5  → 取0
0.5 × 2 = 1.0   → 取1
0.625 = 0.101₂

注意: 十进制小数可能无法精确表示(0.1 是无限循环二进制)
这就是浮点数精度问题的根源

进制速查表

十进制  二进制  十六进制
0       0000    0
8       1000    8
10      1010    A
15      1111    F

负数怎么表示

负数怎么表示

计算机用补码表示负数,让加减法统一为加法运算。

三种表示方案

原码:   最高位符号位(0正1负),其余为绝对值
   +5 = 00000101   -5 = 10000101
反码:   正数同原码;负数符号位不变其余取反
   -5 = 11111010
补码:   正数同原码;负数 = 反码 + 1(计算机采用)
   -5 = 11111011

为什么用补码

1. 加减统一: A - B = A + (-B的补码)
2. 0 唯一表示: 00000000(原码有 +0/-0 问题)
3. 硬件简单: 只需要加法器

示例: 5 - 3 = 5 + (-3)
  00000101 (+5)
+ 11111101 (-3补码)
= 00000010 (+2)  进位丢弃 ✓

补码范围

8位:  -128 ~ 127 (负数比正数多一个)
计算: 正数 0~127,负数 -1~-128
128 个负数 + 128 个正数(含0) = 256

int8:  -2^7 ~ 2^7-1
int16: -2^15 ~ 2^15-1
int32: -2^31 ~ 2^31-1

溢出问题

127 + 1 = -128(8位溢出)
计算机不管数学,按模运算
程序员需注意整数溢出(如 Java 的 Integer.MAX_VALUE + 1)

Python 验证

# Python 整数无限精度,模拟补码
def to_complement(x, bits=8):
    return x & (2**bits - 1)   # 掩码截断

print(bin(to_complement(-5)))    # 0b11111011

小数怎么表示

小数怎么表示

计算机用浮点数(科学计数法)表示小数,遵循 IEEE 754 标准。

定点数 vs 浮点数

定点数: 小数点位置固定,范围小
  如 32.15 固定2位小数: 3215(乘以100存储)
浮点数: 小数点浮动,范围大
  0.000123 = 1.23 × 10⁻⁴(科学计数法)

IEEE 754 单精度(float32)

32位 = 1位符号 + 8位指数 + 23位尾数

数值 = (-1)^符号 × 1.尾数 × 2^(指数-127)

示例: 6.5
  二进制: 110.1 = 1.101 × 2²
  符号=0  指数=2+127=129=10000001  尾数=101...
  → 0 10000001 10100000000000000000000

双精度(double)

64位 = 1符号 + 11指数 + 52尾数
范围更大、精度更高(约15-16位有效数字)

精度问题

0.1 的二进制是无限循环,无法精确表示!
print(0.1 + 0.2)   # 0.30000000000000004

对策:
  1. 比较用容差: abs(a-b) < 1e-9
  2. 金额用整数(分)或 Decimal
  3. 科学计算用高精度库(decimal、mpmath)

特殊值

NaN     非数值(0/0 结果)
Infinity 无穷(溢出)
-0     负零

# Python
float("inf")   # 无穷大
float("nan")   # 非数值

Python 验证

import struct
print(struct.pack(">f", 6.5).hex())  # 40d00000
# 0x40d00000 对应 IEEE 754 表示

字符怎么表示

字符怎么表示

字符通过编码方案映射为二进制:ASCII、Unicode、UTF-8。

ASCII(美国信息交换标准码)

7位编码, 128 个字符
0-31: 控制字符(换行10、回车13)
32-126: 可打印字符
  'A'=65  'a'=97  '0'=48  ' '=32

print(ord('A'))   # 65
print(chr(65))    # 'A'

问题: 无法表示中文等

ASCII 只有 128 个字符
中文数千字、emoji、各国文字
→ 需要更大的编码标准

Unicode 字符集

给所有字符分配唯一编号(码点)
U+4E2D = "中"
U+1F600 = 😀
当前 Unicode 有 15 万+ 字符

UTF-8(可变长编码,主流)

兼容 ASCII(1字节)
中文 3 字节,emoji 4 字节
互联网标准(HTML/JSON 默认)

UTF-16: 2或4字节(Windows/Java内部)
UTF-32: 固定4字节(浪费空间)

中文字节示例

"A"    = 0x41              (1字节)
"中"   = 0xE4B8AD          (3字节)
"😀"   = 0xF09F9880        (4字节)

len("中".encode("utf-8"))   # 3

乱码原因

编码与解码不一致:
  UTF-8 编码被按 GBK 解码 → 乱码
解决: 统一 UTF-8;声明 charset;数据库字符集一致

布尔代数基础

布尔代数基础

布尔代数是数字电路与计算机逻辑的理论基础,只有 0 和 1 两个值。

三种基本运算

与(AND):   全1才1        A·B
或(OR):    有1就1        A+B
非(NOT):   取反          ¬A

真值表(AND):
 0·0=0  0·1=0  1·0=0  1·1=1

真值表(OR):
 0+0=0  0+1=1  1+0=1  1+1=1

复合运算

异或(XOR): 相同为0,不同为1
  A⊕B = (A·¬B) + (¬A·B)
  1⊕0=1  0⊕1=1  1⊕1=0  0⊕0=0
  用途: 加法器、校验、加密

与非(NAND)、或非(NOR): 通用门(可构造一切)

运算定律

交换律: A+B=B+A  A·B=B·A
结合律: (A+B)+C=A+(B+C)
分配律: A·(B+C)=A·B+A·C
德摩根: ¬(A·B)=¬A+¬B  ¬(A+B)=¬A·¬B
吸收律: A+A·B=A  A·(A+B)=A

Python 演示

def and_op(a, b): return a & b
def or_op(a, b): return a | b
def xor_op(a, b): return a ^ b
def not_op(a): return 1 - a

print(xor_op(1, 0))   # 1
print(and_op(1, 1))   # 1

应用

电路设计(与门/或门/非门)
条件判断(AND/OR/NOT)
位运算(掩码、置位、取反)
加密算法基础

逻辑门电路搭建

逻辑门电路搭建

逻辑门是实现布尔运算的电子电路,是构成 CPU 的基本积木。

基本逻辑门

与门(AND):  两个输入都为1,输出1
或门(OR):   任一输入为1,输出1
非门(NOT):  输入取反

符号约定:
AND:  A ———> 输出(A·B)
      B ———>
OR:   A ———> 输出(A+B)
      B ———>
NOT:  A —|o—> 输出(¬A)

复合门

与非门(NAND) = NOT(AND)
或非门(NOR)  = NOT(OR)
异或门(XOR)  = 不同则1

重要: NAND 是通用门
只用 NAND 就能构建 AND/OR/NOT/全加器
(现代芯片制造中 NAND 实现简单)

逻辑门组合示例

# 半加器: A+B(和S, 进位C)
S = A ⊕ B
C = A · B

A=1, B=1 → S=0, C=1 (即 1+1=10₂)

# Python 模拟
def half_adder(a, b):
    s = a ^ b      # 和
    c = a & b      # 进位
    return s, c

print(half_adder(1, 1))   # (0, 1)

逻辑门实现方式

晶体管(CMOS)构建逻辑门
几十亿逻辑门集成在芯片上
功耗: 每开关一次消耗能量(发热来源)

从门到计算机

逻辑门 → 加法器 → ALU(运算器)
逻辑门 → 触发器 → 寄存器/内存
组合 → 控制器、CPU

从逻辑门到加法器

从逻辑门到加法器

用逻辑门搭建加法器,理解运算器(ALU)的基本原理。

半加器

两个输入 A、B
输出: 和(S) 进位(C)
S = A⊕B
C = A·B

A B | S C
0 0 | 0 0
0 1 | 1 0
1 0 | 1 0
1 1 | 0 1

全加器

三个输入: A、B、低位进位Cin
两个输出: 和S、进位Cout
S = A⊕B⊕Cin
Cout = A·B + (A⊕B)·Cin

两个半加器 + 一个或门组成全加器

4位加法器(行波进位)

把4个全加器串联:
最低位Cin=0, 每位Cout接下一位的Cin

  1011
+ 0110
------
  10001(溢出, 4位结果为0001+进位)

# Python 模拟
def full_adder(a, b, cin):
    s = a ^ b ^ cin
    cout = (a & b) | ((a ^ b) & cin)
    return s, cout

def add4(a, b):
    result, carry = 0, 0
    for i in range(4):
        bit_a = (a >> i) & 1
        bit_b = (b >> i) & 1
        s, carry = full_adder(bit_a, bit_b, carry)
        result |= s << i
    return result, carry

print(add4(11, 6))   # (1, 1) → 4位结果为1,有进位

加法器的演进

行波进位: 慢(逐级传播)
超前进位: 并行计算进位(快)
现代 ALU: 优化架构 + 流水线

ALU(算术逻辑单元)

加法器 + 减法(补码) + 逻辑运算(与或非) + 移位
是 CPU 的运算核心

触发器与寄存器

触发器与寄存器

触发器是能"记住"1位数据的电路,组合起来形成寄存器和内存。

为什么需要存储

组合电路: 输出只依赖当前输入(不记忆)
时序电路: 输出依赖历史输入(有记忆)
触发器 = 最基本的记忆单元(1 bit)

SR 锁存器

两个或非门交叉连接
S=置位  R=复位
S=1, R=0 → Q=1(记住1)
S=0, R=1 → Q=0(记住0)
S=1, R=1 → 禁止状态

D 触发器(常用)

时钟边沿采样: 上升沿时 D 的值存入 Q
D=1 → Q=1, 之后D变化Q不变(直到下一个时钟沿)

特性: 数据在时钟控制下"锁存"
应用: 寄存器(多个D触发器并行)

寄存器

8位寄存器 = 8个D触发器并联
CPU 内部有大量寄存器:
  PC(程序计数器): 下一条指令地址
  IR(指令寄存器): 当前指令
  ACC(累加器): 运算暂存
  GPR(通用寄存器): 通用数据(rax/ebx等)

从寄存器到内存

寄存器: CPU内部,最快,容量小(几十~几百字节)
Cache:  SRAM(触发器构成),快,容量MB级
内存:   DRAM(电容+晶体管),慢些,容量GB级
寄存器 → L1 → L2 → L3 → 内存 → 磁盘
(存储金字塔,越靠近CPU越快越贵)

时钟的作用

时钟信号同步所有触发器
每个时钟周期,电路完成一次状态更新
CPU 主频 = 每秒时钟周期数(如 3GHz = 30亿次/秒)

一台计算机的整体骨架

一台计算机的整体骨架

理解计算机的五大部件与总线结构,建立整机视角。

冯·诺依曼体系(1945)

五大部件:
  运算器: 算术逻辑运算(ALU)
  控制器: 取指令、解释、指挥执行
  存储器: 存储程序和数据(内存)
  输入设备: 键盘、鼠标、磁盘等
  输出设备: 显示器、打印机等

核心思想: 存储程序 + 程序控制
程序和数据以二进制存储在存储器中
CPU 按地址取指令自动执行

现代计算机结构

┌───────────── CPU ─────────────┐
│ 控制器 │ 寄存器 │ ALU │ 缓存  │
└──────┬──────────────┬─────────┘
       │ 总线          │
   ┌───┴────┐    ┌─────┴────┐
   │ 内存 RAM │    │ I/O 设备  │
   └────────┘    └──────────┘
       │
   ┌───┴────┐
   │ 磁盘/SSD │ (外存,非直接寻址)
   └────────┘

总线(Bus)

数据总线: 传输数据(宽度=位宽)
地址总线: 传地址(决定寻址范围)
控制总线: 传输控制信号

32位地址总线 → 2³² = 4GB 寻址空间
64位 → 巨大寻址空间

存储程序原理

程序 = 指令序列
编译后存入内存
CPU 循环: 取指 → 译码 → 执行

这使计算机可以"通用"
(换程序 = 换存储内容,不用改硬件)

CPU vs GPU

CPU: 少量强核心,复杂逻辑,通用
GPU: 大量简单核心,并行计算,图形/AI
(现代 AI 训练依赖 GPU 的并行能力)

程序和数据

程序和数据

程序和数据都以二进制存在内存中,计算机如何区分它们?

关键概念

内存中同一串二进制:
  可能是数据(数值)
  也可能是指令(操作码+操作数)
  由"谁引用它"决定含义

0x42 可以是:
  数字 66
  字符 'B'
  一条指令的一部分

程序(指令)格式

指令 = 操作码 + 操作数
例如(简化):
  ADD R1, R2   操作码=ADD, 操作数=R1,R2
  LOAD R1, [100]  从地址100加载到R1
  JMP 200      跳转到地址200

程序的执行过程

高级语言(Python/C) → 编译器 → 汇编
→ 汇编器 → 机器码(二进制指令)
→ 加载到内存 → CPU 执行

存储程序: 指令序列在内存中
数据: 变量、数组在内存数据区

内存中的布局

┌─────────────┐ 高地址
│    栈(局部变量) │
├─────────────┤
│     堆(动态分配) │
├─────────────┤
│ 数据(全局变量)  │
├─────────────┤
│ 代码(指令)     │
└─────────────┘ 低地址
(典型进程内存布局)

冯·诺依曼瓶颈

指令和数据共用总线/内存
取指令和取数据竞争带宽
→ 现代架构用缓存、流水线、指令/数据分离缓存缓解

Python 反汇编示例

import dis

def add(a, b):
    return a + b

dis.dis(add)
# 显示 add 的字节码(类似机器指令)
# LOAD_FAST a / LOAD_FAST b / BINARY_OP + / RETURN_VALUE

指令周期

指令周期

CPU 执行每条指令都要经历取指、译码、执行的循环,称为指令周期。

指令周期四阶段

1. 取指(IF):   按 PC 地址从内存取指令,PC+1
2. 译码(ID):   解析操作码与操作数
3. 执行(EX):   ALU 运算或访问内存
4. 写回(WB):   结果写回寄存器

(简化模型;现代 CPU 还会细分)

关键寄存器

PC(程序计数器): 下一条指令地址
IR(指令寄存器):  当前正在执行的指令
MAR(地址寄存器): 要访问的内存地址
MDR(数据寄存器): 读/写的数据

执行示例(LOAD R1, [100])

1. 取指: PC→内存,取指令→IR, PC++
2. 译码: 识别 LOAD 指令,地址100
3. 执行: MAR=100, 读内存→MDR
4. 写回: MDR→R1

下一周期执行下一条指令

时钟与指令周期

一个指令周期 = 若干时钟周期
主频 3GHz = 每秒 3×10⁹ 个时钟周期
每条指令 2-10+ 个周期(不等)

CPI(每指令周期数): 衡量效率
IPC(每周期指令数): 越高越好(现代 CPU >1)

流水线打破顺序执行

顺序执行: 每条指令完整走完再下一条
流水线: 多条指令重叠执行
  取指1 → 取指2 → 取指3
         译码1 → 译码2
                执行1...
(详见流水线章节)

Python 模拟取指

memory = [("LOAD", 100), ("ADD", 200), ("STORE", 300)]
pc = 0

while pc < len(memory):
    instr = memory[pc]      # 取指
    pc += 1                  # PC+1
    op, addr = instr        # 译码
    print(f"执行: {op} {addr}")   # 执行
    # 具体操作略

时钟频率

时钟频率

时钟频率(主频)决定 CPU 每秒执行多少个基本步骤,是衡量性能的重要指标。

什么是时钟频率

时钟信号: 方波脉冲,同步所有部件
主频 = 每秒时钟周期数
  1GHz = 每秒 10⁹ 个周期

3.5GHz CPU = 每秒 35 亿次时钟跳动
每个周期完成一次基本操作(或流水线阶段)

主频与性能

性能 ≈ 主频 × IPC(每周期指令数)
主频高 ≠ 一定快:
  不同架构每周期效率不同
  IPC 提升比单纯提频更有效
  (如 ARM 低主频高性能)

CPU时间 = 指令数 × CPI × 周期时间
周期时间 = 1/主频

主频发展历史

1980s:  几 MHz
1990s:  100MHz-1GHz
2003:   突破 3GHz(单核天花板,发热)
2005+:  不再疯狂提频 → 多核心
现在:   3-5GHz + 多核 + 加速频率

功耗 ∝ 频率 × 电压²
提频的功耗增长远超性能增长

睿频(Turbo Boost)

动态频率: 单核重负载时超频
多核满载时降频(功耗墙)
现代 CPU 频率是动态变化的

# macOS 查看
sysctl -n machdep.cpu.brand_string

频率 vs 核数

单核频率提升困难 → 多核并行
但: 不是所有任务能并行(Amdahl定律)

加速比 = 1 / ((1-P) + P/N)
P=可并行比例, N=核数

关注点

选 CPU: 看单核性能+核数+功耗
超频: 增加性能但发热
降频: 省电(笔记本常见)

流水线

流水线

流水线让多条指令重叠执行,极大提升 CPU 吞吐率,是现代处理器核心设计。

类比: 工厂流水线

洗衣服(无流水线): 洗→烘→叠→下一件
流水线: 洗衣的同时烘干上一件、折叠更早的...

CPU 流水线: 取指的同时,上一条在译码、
更上一条在执行...

5级流水线示例

IF(取指) → ID(译码) → EX(执行) → MEM(访存) → WB(写回)

时间: 1  2  3  4  5  6  7  8
指令1: IF ID EX MEM WB
指令2:    IF ID EX  MEM WB
指令3:       IF ID  EX  MEM WB
指令4:          IF  ID  EX  MEM

理想吞吐: 每个周期完成1条指令
(单条指令仍要5周期,但吞吐提升5倍)

流水线加速比

k级流水线, n条指令:
顺序: n × k 周期
流水: k + (n-1) 周期
n→∞ 时加速比 → k

现代 CPU 流水线 14-20+ 级

冒险(Hazard)问题

数据冒险: 指令依赖前一条结果
  ADD R1,R2; SUB R3,R1   (SUB需要R1)
  解决: 转发(旁路)、停顿

控制冒险: 跳转指令打断流水
  解决: 分支预测

结构冒险: 资源冲突(同时访存)
  解决: 指令/数据分离缓存

分支预测

遇到 if/跳转,猜测走哪条分支
预测对: 流水不中断(效率高)
预测错: 清空流水线(代价大)
现代预测器准确率 95%+

超流水线 vs 超标量

超流水线: 更多级流水(更细粒度)
超标量: 多个执行单元同时执行多条
(现代 CPU 两者结合 + 乱序执行)

多核心是怎么回事

多核心是怎么回事

多核 CPU 在一个芯片上集成多个处理核心,通过并行提升性能。

单核的困境

频率提升遇到功耗墙(发热)
单核性能增长放缓
→ 用多核并行提升整体性能

多核结构

┌────────── CPU 芯片 ──────────┐
│ 核心0   核心1   核心2   核心3 │
│ ┌────┐  ┌────┐  ┌────┐  ┌────┐│
│ │ALU │  │ALU │  │ALU │  │ALU ││
│ │L1/L2│ │L1/L2│ │L1/L2│ │L1/L2││
│ └────┘  └────┘  └────┘  └────┘│
│     共享 L3 缓存                │
│     内存控制器                  │
└──────────────────────────────┘

每个核心: 独立的寄存器、ALU、L1/L2
共享: L3 缓存、内存、I/O

操作系统视角

每个核心可同时运行一个线程
4核8线程 = 超线程(每核2线程)
操作系统调度: 把任务分配给核心

# 查看核数
import os
print(os.cpu_count())   # 8

并行能力限制(Amdahl 定律)

加速比 = 1 / ((1-P) + P/N)
P=可并行比例, N=核数

P=90%, 4核: 加速比≈3.07
P=90%, 16核: 加速比≈6.4
即使无限核,加速比也≤ 1/(1-P)

并发 vs 并行

并发: 交替执行(单核也可)
并行: 同时执行(需多核)
多线程 ≠ 一定并行(可能抢同一核)

多核编程

# Python 多线程(GIL 限制CPU并行)
# 用多进程实现真并行
from multiprocessing import Pool

def work(x):
    return x * x

with Pool(4) as p:
    print(p.map(work, range(10)))

# 高效方案: 多进程/NumPy(C扩展)/GPU

异构多核(大小核)

性能核(P核): 高主频,重负载
能效核(E核): 省电,后台任务
Apple M 系列、Intel 12代+ 采用
系统调度: 轻任务用E核,重任务用P核

存储金字塔

存储金字塔

计算机存储系统按速度与容量构成金字塔结构,兼顾速度与成本。

金字塔结构

        速度↑ 容量↓ 成本↑
  ┌──────────────┐
  │  寄存器       │  KB级   ~1周期
  │  L1 缓存      │  32-64KB ~2周期
  │  L2 缓存      │  256KB-1MB ~10周期
  │  L3 缓存(共享) │  8-32MB ~40周期
  │  内存(RAM)    │  8-64GB  ~100周期+
  │  SSD          │  512GB-2TB 慢1000x
  │  机械硬盘     │  数TB   最慢
  └──────────────┘
        速度↓ 容量↑ 成本↓

为什么需要金字塔

没有一种存储器: 又快又大又便宜
寄存器快但极小(几十字节)
内存大但比CPU慢100倍
→ 用缓存层弥合速度差距(局部性原理)

局部性原理

时间局部性: 刚访问的数据很快会再用(循环)
空间局部性: 附近的数据会被访问(数组遍历)

代码示例:
for i in range(n):     # i 反复访问(时间)
    sum += arr[i]      # arr 连续访问(空间)

缓存正是利用局部性预取数据

存储技术

SRAM: 触发器构成,快,贵(缓存)
DRAM: 电容+晶体管,需刷新,较便宜(内存)
Flash: 电子存储,断电保存(SSD/U盘)
磁盘: 磁记录,容量大,机械(HDD)

内存 vs 硬盘: 内存断电丢失,硬盘持久

缓存的意义

命中率高(>95%)时,内存慢不再是瓶颈
程序性能优化第一原则: 缓存友好
  (数据局部性、避免随机访问)

Python 缓存感知示例

# 行优先遍历(快,空间局部性好)
for i in range(n):
    for j in range(n):
        s += matrix[i][j]

# 列优先遍历(慢,跳着访问)
for j in range(n):
    for i in range(n):
        s += matrix[i][j]

缓存命中与缓存未命中

缓存命中与缓存未命中

缓存(Cache)是 CPU 与内存之间的高速缓冲,命中与未命中决定性能。

基本原理

CPU 访问内存前先查缓存:
  命中(Hit): 缓存有数据 → 快速返回
  未命中(Miss): 缓存没有 → 去内存取,并存入缓存

命中率 = 命中次数 / 总访问次数
命中率高 → 有效访问时间接近缓存速度

缓存行(Cache Line)

缓存以块为单位加载(通常 64 字节)
访问 arr[0] 时,arr[0..15] 一起载入
(空间局部性的硬件体现)

Python 中一个整数28字节,缓存行装约2个
numpy 数组连续存储,缓存友好(性能快的原因之一)

映射方式

直接映射: 每个内存块只有1个缓存位置(冲突多)
全相联:   任意位置(硬件复杂)
组相联:   一组内任意位置(折中,现代采用)

替换策略

LRU(最近最少使用): 淘汰最久未用的
FIFO: 先入先出
随机: 简单

组相联缓存常用 LRU 近似算法

未命中的代价

CPU 可能需要停顿几十~几百个周期
(现代 CPU 用乱序执行/预取缓解)

优化: 让程序数据访问有局部性
避免"缓存抖动"(两个数据竞争同一缓存行)

性能对比示例

# 快: 顺序访问(缓存友好)
total = sum(arr)          # O(n)

# 慢: 随机访问大数组(缓存未命中)
for _ in range(n):
    idx = random.randrange(n)
    total += arr[idx]

# 大数组遍历 vs 小数组(超出缓存容量时变慢)
# 测试: numpy 与 Python 列表的速度差异
# 很大程度来自缓存利用(连续内存)

多级缓存

L1(最快最小) → L2 → L3(最大最慢)
逐级查找,未命中逐级下沉
L3 未命中才访问内存

内存地址与寻址

内存地址与寻址

内存按字节编址,CPU 通过地址访问数据,理解寻址是编程与调试的基础。

按字节编址

每个字节一个唯一地址(0, 1, 2, ...)
地址用十六进制表示更直观:
  0x0000, 0x0004, ...

内存: [0x1000]=0x2A [0x1001]=0x3B ...
32位地址空间: 0x00000000 ~ 0xFFFFFFFF (4GB)
64位: 巨大的地址空间

变量与地址

# C 语言
int x = 42;        // 变量 x 在某个内存地址
int *p = &x;       // & 取地址, p 存 x 的地址

# Python
x = 42
print(id(x))       # 对象的地址(CPython)
# 140736189032192

寻址方式(汇编视角)

立即寻址: 直接给值
  mov eax, 42

寄存器寻址: 值在寄存器
  mov eax, ebx

直接寻址: 地址在指令中
  mov eax, [0x1000]

寄存器间接: 地址在寄存器
  mov eax, [ebx]

变址寻址: 基址+偏移(数组访问)
  mov eax, [ebx + ecx*4]

端序(字节序)

大端: 高字节在低地址(网络协议)
小端: 低字节在低地址(x86/ARM 主流)

整数 0x12345678 在小端内存:
地址: [0]=78 [1]=56 [2]=34 [3]=12

# Python
import struct
print(struct.pack("

地址空间划分

用户态 vs 内核态(操作系统保护)
代码段/数据段/堆/栈
虚拟地址 vs 物理地址(MMU 转换)
每个进程有独立虚拟地址空间(隔离)

常见错误

空指针: 地址为0(无效)
野指针: 指向已释放内存
越界: 访问数组边界外
(现代语言/运行时帮助检查,但理解原理仍重要)

I/O 的三种方式

I/O 的三种方式

CPU 与外设(I/O 设备)交互有三种方式:程序查询、中断、DMA。

为什么 I/O 慢

CPU: 纳秒级操作
外设: 毫秒级(键盘/磁盘/网络)
速度差异巨大 → 不能让 CPU 干等

方式一: 程序查询(轮询)

CPU 不断检查设备状态寄存器
直到设备就绪再传输数据

缺点: CPU 空转等待(浪费)
适用: 简单设备、学习理解

while True:
    if device_ready():
        data = read_device()
        break    # CPU 一直轮询

方式二: 中断(Interrupt)

设备就绪时主动"通知"CPU
CPU 暂停当前程序 → 执行中断处理程序 → 恢复

流程:
  1. 设备发中断请求
  2. CPU 保存现场(寄存器/PC)
  3. 跳转中断处理程序(设备驱动)
  4. 处理完恢复现场

优点: CPU 可以干别的事(高效)
应用: 键盘、鼠标、网卡等几乎所有设备

方式三: DMA(直接内存访问)

DMA 控制器绕过 CPU 直接在外设与内存间传数据
CPU 只需启动传输,完成后收到中断

流程:
  1. CPU 配置 DMA(源、目标、长度)
  2. DMA 控制器搬运数据(CPU 空闲)
  3. 完成 → 中断通知 CPU

应用: 磁盘读取、网卡收包、显卡(大块数据)

三种方式对比

查询:   CPU 等,慢,简单(不实用)
中断:   CPU 被通知,中等效率(字节级)
DMA:    CPU 几乎不参与,高效(块级)

实际系统: 中断 + DMA 组合
(设备完成DMA后发中断通知)

现代 I/O 架构

中断与DMA → 现代还有:
  内存映射I/O(MMIO)
  轮询模式(高性能网卡: DPDK)
  异步I/O(操作系统层面: epoll/io_uring)
  GPU 大规模并行 I/O

一条指令的完整旅程

一条指令的完整旅程

从源代码到指令执行,完整跟踪一条指令在计算机中的旅程。

第一站: 源代码

# C 代码
int add(int a, int b) {
    return a + b;
}

# 汇编(简化)
add:
    mov eax, [esp+4]    ; 取 a
    add eax, [esp+8]    ; eax += b
    ret                 ; 返回

# 机器码
8B 44 24 04  03 44 24 08  C3

编译链路

源码 → 编译器 → 汇编 → 汇编器 → 机器码(可执行文件)
可执行文件 → 加载器 → 装入内存

第二站: 加载进内存

操作系统把程序装入内存:
  代码段: 机器指令
  数据段: 全局变量
  栈: 调用信息
  堆: 动态内存

CPU 的 PC 指向第一条指令地址

第三站: 指令周期执行

以 mov eax, [esp+4] 为例:
1. 取指(IF):  PC→内存,取 8B 44 24 04 → IR
2. 译码(ID):  识别 mov eax, [esp+4]
3. 执行(EX):  计算地址 esp+4,读内存
4. 写回(WB):  值写入 eax 寄存器
5. PC 指向下一条(add 指令)

循环执行直至程序结束

第四站: 流水线加速

多条指令在流水线中重叠:
  add 取指时,mov 在译码,前面指令在执行
分支预测预测跳转,缓存提供操作数
(现代 CPU 一次执行几十条指令的不同阶段)

第五站: 性能因素

影响一条指令实际耗时的因素:
  缓存命中(操作数是否在缓存)
  分支预测(是否跳转)
  流水线冒险(数据依赖)
  主频(每秒周期数)
  乱序执行(重排指令提高效率)

理解的意义

写代码时心中有"指令旅程":
  循环/分支 → 分支预测与流水线
  数组遍历 → 缓存局部性
  函数调用 → 栈与寄存器
  递归 → 栈深度
  全局变量/并发 → 内存模型

为什么计算机只认识 0 和 1

为什么计算机只认识 0 和 1

再次从物理与逻辑层面理解:二进制是计算机的最优选择。

物理层面

任何物理量做两态区分最可靠:
  电压: 高/低
  电流: 有/无
  磁场: 两个方向
  光:   通/断

两态判断容错大: 3.3V 算1, 0V 算0
中间值容易受噪声干扰(十进制10态难以稳定区分)

逻辑层面

布尔代数天然二值(0/1)
与、或、非运算简单完备
由逻辑门构建任意复杂电路
(可计算性: 图灵完备只需二值逻辑)

工程层面

电路简单: 开关即可表示
制造容易: 晶体管就是开关
纠错方便: 两态易检测错误
成本低: 大规模集成

多进制尝试(三值计算机)存在但成本高、未成主流

二进制的代价

表示数字需要更多位(效率低)
  十进制 999 → 二进制 12位
但硬件速度弥补: 每秒数十亿次操作

信息论: 二进制是信息编码的自然基础
(1 bit = 一个二元选择)

从 0/1 到复杂世界

0/1(位) → 字节 → 整数/字符 → 数据结构
→ 指令 → 程序 → 操作系统 → 应用 → 互联网/AI

所有复杂系统都由最基础的两个符号构建
这既是限制,也是统一之美

总结

1. 物理两态可靠、便宜、易实现
2. 逻辑二值完备、可组合一切
3. 工程上晶体管开关天然支持
4. 现代计算机 = 数十亿二值开关的精确协作
这就是"为什么计算机只认识 0 和 1"的完整答案
第 1 / 22 节