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
01000001ASCII 字符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:
读取指令
解释指令
根据指令读写数据

这带来两个重要后果:

  1. 计算机可以执行不同程序,只要把不同指令放进内存。
  2. 程序错误或攻击也可能来自“把数据当成指令”或“把指令区域错误修改”。

组成原理里很多安全、系统、编译知识都能从这里继续展开。

8. 指令、地址和数据的关系

CPU 执行指令时,经常要处理地址。

例如:

load address -> register
store register -> address

地址可以理解为内存位置编号。数组随机访问快,是因为地址可以直接算出来:

元素地址 = 起始地址 + 下标 * 元素大小

链式结构可能慢,是因为下一个节点地址要从当前节点里读出来:

读当前节点
-> 拿到 next 地址
-> 跳到 next 地址
-> 再读下一个节点

这会破坏连续访问的局部性。

所以数据结构不是纯数学对象,它和硬件存储方式强相关。

9. 组成原理如何连接四大件

组成原理是另外三门课的底座。

课程和组成原理的连接
数据结构连续内存、指针、缓存行、地址计算决定访问成本
操作系统进程、虚拟内存、系统调用、IO、中断都建立在硬件机制上
计算机网络网卡、DMA、中断、缓冲区、字节序影响网络传输
算法分析理论复杂度之外,还要考虑常数、缓存、分支、内存带宽

例如同样是 O(n):

顺序遍历连续数组
随机遍历分散节点

理论复杂度一样,实际性能可能差很多。组成原理解释的就是这种“复杂度之外”的硬件成本。

10. 联系实际:遇到程序慢时从硬件层怎么想

假设一个程序慢,不要只问“算法是不是 O(n^2)”。还要问:

  1. 数据量多大?
  2. 数据是连续的还是分散的?
  3. 访问是顺序的还是随机的?
  4. CPU 忙不忙?
  5. 是否大量等待内存、磁盘或网络?
  6. 是否频繁 cache miss?
  7. 是否有大量分支且难预测?
  8. 是否有多核之间共享写入?
  9. 是否被 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. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 为什么计算机最终只处理 0 和 1,却能表示数字、字符和指令?
  2. 一行高级代码大致会变成哪些硬件动作?
  3. CPU 的取指、译码、执行、访存、写回各是什么意思?
  4. 为什么需要寄存器、Cache、内存、磁盘这样的存储层次?
  5. 为什么数组、链表、文件、网络 IO 的性能都能从数据移动角度解释?
  6. 为什么组成原理是数据结构、操作系统、网络的共同底座?
  7. 遇到程序慢时,如何从 CPU、内存、Cache、IO 几个方向提出问题?

后续章节会把这里的地图拆开:先讲数据如何表示,再讲 CPU 如何执行,最后讲存储层次和 IO 如何决定真实性能。

延伸阅读