from - zero · computer-organization
00. 从 0 开始理解计算机组成原理
0. 本章先解决什么问题
你写的程序看起来是函数、变量、循环、对象、文件、网络请求。机器真正执行时,看到的是另一套东西:
位模式
内存地址
机器指令
寄存器
加法器和逻辑电路
缓存
总线
设备控制器
计算机组成原理要解决的问题是:
高级程序如何最终变成硬件能执行的动作?
如果不理解这一层,很多现象只能死记:
- 为什么整数会溢出?
- 为什么小数计算会有误差?
- 为什么数组顺序遍历快,链表可能慢?
- 为什么 CPU 很高不一定是“算法复杂”,也可能是缓存和分支问题?
- 为什么 IO 慢时 CPU 可能并不忙?
- 为什么多核并发会牵涉缓存一致性?
本章先建立最小地图。后面的章节再逐层展开。
先看最小硬件模型:程序最终会被拆成存储、计算、控制和输入输出。
这张图怎么读
这张图不是把硬件名字摆在一起,而是在回答一个最小问题:
一段程序要运行,至少需要哪些硬件角色协作?
读图时按四条线看:
| 线索 | 要问什么 |
|---|---|
| 存储 | 指令和数据放在哪里,离 CPU 多远 |
| 计算 | 哪个部件真正对 bit 做运算 |
| 控制 | 下一条指令怎么决定,分支怎么改变路径 |
| 输入输出 | 数据如何进出机器,设备如何通知 CPU |
后面所有组成原理章节,都是把这四条线展开。整数溢出属于表示和计算,Cache 属于存储层次,指令执行属于控制,DMA 和中断属于输入输出。
1. 这门课到底在讲什么
计算机组成原理研究一台计算机怎样组织硬件,来完成三件事:
表示信息
执行计算
搬运数据
这三件事对应软件里的大量问题:
| 硬件问题 | 软件里表现成什么 |
|---|---|
| 信息如何表示 | 整数范围、浮点误差、字符编码、地址 |
| 计算如何执行 | 指令、分支、函数调用、流水线 |
| 数据如何搬运 | 内存访问、缓存命中、IO、网络发送 |
| 设备如何协作 | 中断、DMA、总线、驱动 |
| 成本如何产生 | 等待、复制、缓存未命中、带宽瓶颈 |
所以组成原理不是“硬件工程师才要懂”。它是 CS 学生理解软件底层行为的基础。
2. 最小模型:存储、计算、控制、输入输出
一台通用计算机至少需要四类能力:
| 部分 | 作用 | 类比 |
|---|---|---|
| 存储器 | 保存程序和数据 | 纸和文件柜 |
| 运算器 | 做算术和逻辑运算 | 计算器 |
| 控制器 | 决定下一步执行哪条指令 | 指挥者 |
| 输入输出 | 和外部世界交换数据 | 眼睛、手、网络、磁盘 |
抽象成一个流程:
输入
-> 存储到内存
-> CPU 读取指令和数据
-> CPU 执行计算和控制
-> 结果写回内存或设备
-> 输出
现代 CPU 内部通常包含:
- 寄存器:CPU 里最快的小存储。
- ALU:算术逻辑单元,做加减、比较、位运算。
- 控制单元:解释指令并发出控制信号。
- 多级 Cache:缓解 CPU 和内存速度差。
- 流水线、分支预测、乱序执行等优化结构。
你不需要一开始掌握所有细节,但要先记住:
CPU 不是直接“理解程序”,它只按指令一步步操作位和地址。
反例:机器快,不代表所有程序都会快
硬件参数经常让人误判性能。例如 CPU 主频更高、内存更大、磁盘更快,都不自动保证某个程序变快。
| 程序主要瓶颈 | 升级错方向会怎样 |
|---|---|
| 分支预测失败多 | 加内存不一定有明显帮助 |
| 随机内存访问多 | 提高 CPU 主频可能仍在等内存 |
| 小文件同步写多 | 加 CPU 核心不能消除刷盘等待 |
| 单线程依赖链长 | 增加核心数不一定提升 |
| 网络等待多 | 本机硬件升级可能改变很小 |
组成原理的价值在于把“快”拆成可解释的资源:
计算单元是否忙?
数据是否及时到达?
控制流是否顺畅?
IO 是否在排队?
只有知道瓶颈落在哪条线,硬件知识才会变成有效判断,而不是参数崇拜。
3. 程序如何变成机器能执行的东西
不同语言、运行时和系统的路径不同,但大方向相似:
源代码
-> 编译 / 解释 / 运行时处理
-> 指令或中间指令
-> 机器指令
-> CPU 执行
机器指令通常表达的动作很低级:
从地址 A 读数据到寄存器 R1
从地址 B 读数据到寄存器 R2
把 R1 和 R2 相加
把结果写回地址 C
如果比较结果为真,跳转到某条指令
你在高级语言里看到:
c = a + b
硬件层面更像:
load a -> register1
load b -> register2
add register1, register2 -> register3
store register3 -> c
这就是组成原理的关键视角:
把高级动作拆成硬件能执行的基本动作。
4. 位:所有信息的共同底座
硬件最容易稳定地区分两种状态,比如:
- 高电平 / 低电平。
- 导通 / 断开。
- 磁化方向不同。
- 电荷有 / 无。
于是计算机用 0 和 1 表示信息。
但 0 和 1 本身没有意义。意义来自解释规则:
| 位模式 | 解释规则 | 得到的意义 |
|---|---|---|
01000001 | 无符号整数 | 65 |
01000001 | ASCII 字符 | A |
01000001 | 指令的一部分 | 某个操作码或操作数 |
01000001 | 协议字段 | 某种状态或长度 |
所以后面所有知识都离不开一句话:
bit pattern + interpretation = information
没有解释规则,就只有位;有了解释规则,才有整数、字符、浮点数、地址和指令。
5. CPU 执行指令的基本循环
CPU 执行程序的基本循环可以简化为:
取指 fetch
-> 译码 decode
-> 执行 execute
-> 访存 memory
-> 写回 write back
-> 更新下一条指令位置
每一步的意思:
| 阶段 | 做什么 |
|---|---|
| 取指 | 根据程序计数器拿到下一条指令 |
| 译码 | 解析这条指令要做什么、用哪些操作数 |
| 执行 | ALU 计算、比较、跳转判断 |
| 访存 | 需要时读写内存或 Cache |
| 写回 | 把结果写入寄存器或内存 |
程序计数器保存“下一条指令在哪里”。分支、循环、函数调用、异常都会改变它。
一个循环可以理解成:
执行一段指令
判断条件
如果继续,程序计数器跳回前面
如果结束,程序计数器走向后面
这就是为什么控制流和组成原理是连在一起的。
6. 存储层次:为什么数据不放在一个地方
理想情况下,我们希望存储同时满足:
- 无限大。
- 极快。
- 极便宜。
- 永不丢失。
现实做不到。硬件只能分层:
寄存器
-> L1 Cache
-> L2 Cache
-> L3 Cache
-> 主内存
-> SSD / 磁盘
-> 网络或外部存储
越靠近 CPU:
- 越快。
- 越小。
- 越贵。
越远离 CPU:
- 越慢。
- 越大。
- 单位容量更便宜。
这解释了很多程序性能现象:
| 现象 | 硬件解释 |
|---|---|
| 顺序访问快 | 连续数据容易进入 Cache |
| 随机访问慢 | 经常 cache miss |
| 大对象遍历慢 | 数据跨越更多缓存行 |
| 第一次读文件慢,第二次快 | 页缓存或存储缓存命中 |
| CPU 等待 IO | 数据在慢设备上,还没搬到内存 |
程序不是只在“CPU 上跑”。程序大量时间花在等数据到达 CPU。
7. 冯诺依曼模型:程序和数据都在存储器里
现代通用计算机常用一个重要思想:
程序也是数据。
指令和普通数据一样,最终都以 bit 的形式存储。
简化图:
内存:
[指令][指令][数据][数据][指令][数据]
CPU:
读取指令
解释指令
根据指令读写数据
这带来两个重要后果:
- 计算机可以执行不同程序,只要把不同指令放进内存。
- 程序错误或攻击也可能来自“把数据当成指令”或“把指令区域错误修改”。
组成原理里很多安全、系统、编译知识都能从这里继续展开。
8. 指令、地址和数据的关系
CPU 执行指令时,经常要处理地址。
例如:
load address -> register
store register -> address
地址可以理解为内存位置编号。数组随机访问快,是因为地址可以直接算出来:
元素地址 = 起始地址 + 下标 * 元素大小
链式结构可能慢,是因为下一个节点地址要从当前节点里读出来:
读当前节点
-> 拿到 next 地址
-> 跳到 next 地址
-> 再读下一个节点
这会破坏连续访问的局部性。
所以数据结构不是纯数学对象,它和硬件存储方式强相关。
9. 组成原理如何连接四大件
组成原理是另外三门课的底座。
| 课程 | 和组成原理的连接 |
|---|---|
| 数据结构 | 连续内存、指针、缓存行、地址计算决定访问成本 |
| 操作系统 | 进程、虚拟内存、系统调用、IO、中断都建立在硬件机制上 |
| 计算机网络 | 网卡、DMA、中断、缓冲区、字节序影响网络传输 |
| 算法分析 | 理论复杂度之外,还要考虑常数、缓存、分支、内存带宽 |
例如同样是 O(n):
顺序遍历连续数组
随机遍历分散节点
理论复杂度一样,实际性能可能差很多。组成原理解释的就是这种“复杂度之外”的硬件成本。
10. 联系实际:遇到程序慢时从硬件层怎么想
假设一个程序慢,不要只问“算法是不是 O(n^2)”。还要问:
- 数据量多大?
- 数据是连续的还是分散的?
- 访问是顺序的还是随机的?
- CPU 忙不忙?
- 是否大量等待内存、磁盘或网络?
- 是否频繁 cache miss?
- 是否有大量分支且难预测?
- 是否有多核之间共享写入?
- 是否被 IO 设备速度限制?
如果 CPU 不忙,程序慢可能不是计算问题,而是等待数据。
如果 CPU 很忙,也可能不是算法问题,而是缓存未命中、分支失败、原子操作竞争、数据布局差。
组成原理给你的不是“万能优化技巧”,而是定位方向:
瓶颈在计算、内存、缓存、总线、设备,还是同步?
手推:同一个 O(n),为什么硬件成本可以差一个数量级
假设两个任务都处理 1000 万个元素:
- 任务 A:顺序扫描连续数组
- 任务 B:沿着指针随机跳到下一个节点
理论上它们都可能写成 O(n)。但硬件看到的是两种完全不同的访问:
| 访问方式 | 硬件效果 |
|---|---|
| 连续数组 | cache line 一次带来相邻多个元素,预取更容易 |
| 随机链式节点 | 每一步先等当前节点,再知道下一个地址 |
顺序扫描像这样:
读地址 1000
读地址 1004
读地址 1008
读地址 1012
CPU 可以猜到你接下来还会读附近的数据。
链式访问像这样:
读节点 A -> 得到 next = 8392016
读节点 B -> 得到 next = 120088
读节点 C -> 得到 next = 9917720
下一个地址依赖当前内存读取结果,CPU 很难提前准备。于是同样的 O(n),可能一个主要受内存带宽限制,另一个主要受内存延迟和 cache miss 限制。
这就是组成原理必须和数据结构一起学的原因:复杂度告诉你增长趋势,硬件模型告诉你每一步的真实成本。
边界条件:CPU 忙不等于计算有效
看到 CPU 使用率高,初学者容易认为:
程序一定在做很多有效计算。
但 CPU 可能忙在很多“非业务计算”上:
| 忙的内容 | 典型原因 |
|---|---|
| 分支失败后的重做 | 数据模式让分支不可预测 |
| cache miss 等待 | 随机访问、工作集过大 |
| 原子操作和锁竞争 | 多核频繁争同一 cache line |
| 内核态处理 | 系统调用、网络协议栈、驱动 |
| 上下文切换 | 线程过多、短任务过多 |
| 忙等 | 循环检查条件而不让出 CPU |
所以硬件层排障要问:
CPU 在用户态还是内核态?
指令是否真的在推进业务逻辑?
是否大量等待内存或同步?
是否存在频繁切换和重试?
“CPU 高”只是现象,不是结论。
建模卡:把“程序变慢”落到硬件四件事
遇到一个程序慢,先不要只说“算法不行”。从组成原理视角,把它拆成四类硬件动作:
| 动作 | 它在消耗什么 | 典型慢法 |
|---|---|---|
| 取指令 | 指令缓存、分支路径 | 代码路径复杂、分支预测失败 |
| 做计算 | 寄存器、ALU、执行单元 | 计算密集、向量化不足、依赖链长 |
| 取数据 | 缓存、内存、地址转换 | 随机访问、缓存未命中、TLB 压力 |
| 等设备 | 总线、控制器、DMA、中断 | 磁盘/网络等待、复制过多、队列堆积 |
练习:拿一个“遍历大量数据”的任务,分别假设瓶颈在计算、内存、IO。你会提出哪些不同优化?如果三个假设的优化手段完全不同,就说明硬件模型已经在帮你避免盲目调参。
11. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 为什么计算机最终只处理 0 和 1,却能表示数字、字符和指令?
- 一行高级代码大致会变成哪些硬件动作?
- CPU 的取指、译码、执行、访存、写回各是什么意思?
- 为什么需要寄存器、Cache、内存、磁盘这样的存储层次?
- 为什么数组、链表、文件、网络 IO 的性能都能从数据移动角度解释?
- 为什么组成原理是数据结构、操作系统、网络的共同底座?
- 遇到程序慢时,如何从 CPU、内存、Cache、IO 几个方向提出问题?
后续章节会把这里的地图拆开:先讲数据如何表示,再讲 CPU 如何执行,最后讲存储层次和 IO 如何决定真实性能。