计算机组成原理
计算机组成原理
计算机组成原理研究计算机的硬件组成、工作原理与体系结构,是计算机科学的核心基础课,帮助理解"计算机如何工作"。
本教程包含
- 数据表示:二进制、进制转换、负数/小数/字符表示
- 电路基础:布尔代数、逻辑门、加法器、触发器与寄存器
- 整机结构:计算机骨架、程序与数据、指令周期、时钟频率
- 性能与存储:流水线、多核心、存储金字塔、缓存、内存寻址
- 输入输出: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"的完整答案