“计算机组成原理”这四个字,在课程表上看着平平无奇,但它属于那种“学的时候觉得抽象、用起来发现真香”的课。而“计算机的基本组成”又是整门课的地基——运算器、控制器、存储器、输入输出设备,加上把它们串起来的总线,这几块东西怎么分工、怎么配合、一条指令怎么从内存被搬到 CPU 里执行完再写回去,全在这一章里说清楚了。这篇东西我打算按一个干过几年活、又回头补过这门课的人的视角来写:不背名词,讲协作;不堆概念,讲“为什么是这个设计”。如果你正在学《计算机组成原理》,或者你是写代码的、想知道自己天天调用的那行语句在硬件上到底发生了什么,又或者你正为期末和实验发愁,这篇内容都能直接用。
我先把话说在前面:这一章的知识点看着碎,其实骨架非常硬。抓住“数据流”和“控制流”两条线,剩下的 MAR、MDR、PC、IR、ACC 这些缩写,都只是挂在骨架上的肉。学完你应该能做到:随便给你一条指令,你能在纸上把它从取指到写回的全过程推演一遍,并且说清楚每个机器周期里数据从哪来、到哪去、由谁发命令。
1. 先搞清楚这章到底在讲什么
1.1 冯·诺依曼结构真正解决的是哪三个问题
很多人背“存储程序、程序控制”这八个字,背完就忘,因为不知道它在解决什么。回到上世纪四十年代那个语境,当时造计算机的人面对三个非常具体的问题:第一,指令怎么告诉机器;第二,指令和数据怎么存放;第三,机器怎么一步步自动往下走,而不是算一步、人工拨一次开关。
冯·诺依曼结构的答案是:把指令和数据都用二进制表示,并且同等地位地存放在同一个存储器里,按地址访问;用程序计数器指向下一条要执行的指令;机器按“取指—分析—执行”的节奏循环,自动推进。这个设计最妙的地方在于“同等地位”四个字——它意味着存储器不需要区分哪个格子放的是指令、哪个放的是数据,硬件结构大大简化,而且程序可以被当作数据来搬运和修改,编译、加载、动态链接这些后来的东西才有了立足点。
代价也很明显:CPU 和存储器之间只有一条通道(总线),取指令和取数据都得排队走这条路,这就是大名鼎鼎的冯·诺依曼瓶颈。你后面学到的 Cache、多级存储、指令预取、流水线,本质都在想办法缓解这个瓶颈。所以这一章不是孤立的“认零件”,它是整本书后续所有优化手段的问题源头。
1.2 五大部件的分工,别只背名字要背协作
运算器、控制器、存储器、输入设备、输出设备,这五个词谁都会背,但真正有用的是搞清楚它们之间谁给谁发命令、谁给谁送数据。用一句大白话概括:控制器是脑子,运算器是手,存储器是仓库,输入输出是收发室,总线是走廊和传送带。
关键在于,原始冯·诺依曼结构是以运算器为中心的——数据要从存储器先搬到运算器,算完再搬回存储器,输入输出设备的数据也得先经过运算器中转。这个设计在今天看来很蠢,因为运算器成了交通堵点。现代计算机改成了以存储器为中心,数据可以在任意两个部件之间通过总线直接流动,运算器只负责“算”,不负责“中转”。你去看现在的主板框图,CPU、内存、外设挂在同一套总线上(或者通过北桥/内存控制器分层挂载),就是这个思想的延续。
这个转变为什么重要?因为它直接决定了你写代码时的一个直觉:数据搬动的代价往往比计算本身更大。一次内存访问的开销可能是几百个时钟周期,而一次加法只要一个周期。凡是理解这一点的人,写循环时都会下意识地考虑数据布局,而不是无脑嵌套遍历。
1.3 写代码的人为什么也得看这一章
热词里有一条“学软件的要学计算机组成原理”,这个问题每年都有人在问。我的回答很直接:不是必修课表逼你,是你迟早会被它逼回来。
举几个我亲身遇到或者带人时遇到的场景。数组按行遍历和按列遍历,同样的逻辑、同样的数据量,性能能差三到十倍,原因就是 Cache 按块加载,跨行访问把局部性彻底打碎了。再比如位运算优化,x & (x-1)能消掉最低位的 1,这个技巧背后是 ALU 只做与或非加移位这些基本操作,编译器不会替你发明指令。还有并发编程里的内存可见性问题,本质上是写缓冲和 Cache 一致性协议在作祟,而这些概念第一次出现的地方,就是“存储器层次结构”那一节。
我不主张为了炫技去啃硬件细节,但把这一章的骨架图装进脑子,性价比高得离谱。它不占你多少时间,却能在你排查性能问题、读懂反汇编、理解内存模型的时候,反复给你回报。
2. 五大部件的拆解与关键寄存器
2.1 运算器:ALU 到底能算什么,PSW 又记了什么
运算器的核心是算术逻辑单元(ALU),它支持的操作类型其实非常有限:加、减、与、或、非、异或、移位、比较。所以你在高级语言里写的乘除法、浮点运算、取模,最终都要被翻译成这些基本操作或者由专门的部件(乘法器、浮点单元)来承担。理解这一点,你就能理解为什么整数除法的代价远高于加减,为什么模运算在某些场景下会被优化成位与。
围绕 ALU 一般还有几个关键寄存器:累加器 ACC存放一个操作数和运算结果,乘商寄存器 MQ在乘除运算中配合使用,操作数寄存器 X暂存另一个操作数,以及程序状态字寄存器 PSW。PSW 是最容易被忽略但最重要的一个,它保存进位标志 C、溢出标志 O、结果为零标志 Z、符号标志 S 等等。
这些标志位不是摆设。条件跳转指令就是靠它们工作的:if (a > b)编译出来往往是先做一次减法或者比较,然后根据 PSW 里的符号位和零标志决定跳不跳。而且标志位的更新是有副作用的,某些指令会改标志位、某些不会,这在写汇编或者做指令级优化时是必须留意的细节。我在看一些底层代码时,经常发现有人以为比较指令不改变状态,结果在指令重排后踩了坑。
注意:ALU 不直接和存储器打交道。它需要的数据必须先被搬进寄存器,算完的结果也先落在寄存器里。这个“寄存器中转”的设计是后续所有指令周期分析的基础。
2.2 控制器:PC 和 IR 是怎么配合把程序跑起来的
控制器的职责只有一个:按时序发出正确的控制信号。它内部最核心的两个寄存器是程序计数器 PC和指令寄存器 IR。PC 存放的是下一条要执行的指令的地址,IR 存放的是当前正在执行的指令本身。
整个循环的骨架是:把 PC 的内容送到地址线上,从存储器读出指令放进 IR,然后 PC 自动加一(或者加指令长度),接着对 IR 里的操作码进行译码,产生一串控制信号去指挥其他部件工作。这里有个细节很多人会卡:PC 加的是“一条指令的长度”,不是简单加一。在定长指令集里确实是加一或者加四,但在变长指令集(比如现在主流的那些)里,PC 的增量取决于当前指令占了多少字节,所以要等指令译码之后才能确定。
控制器的实现方式分两大类:硬布线控制器和微程序控制器。硬布线用组合逻辑电路直接产生控制信号,速度快但改起来要重新设计电路;微程序把控制信号存在一个只读存储器里,用“微指令”来解释“机器指令”,灵活但慢一些。理解这两条路线的取舍,对你后面看处理器的设计资料很有帮助——很多简单指令集用硬布线,复杂指令集倾向微程序或者混合方案。
2.3 存储器:MAR 和 MDR 决定了地址空间和字长
存储器部分最容易出计算题,也最容易背混。核心是两个寄存器:MAR(存储器地址寄存器)和MDR(存储器数据寄存器)。
MAR 的位数决定了可寻址的存储单元个数,因为 N 位地址能表示 2^N 个不同地址。MDR 的位数等于存储字长,也就是一次能读写多少位。两者相乘才是存储容量。这个关系必须记牢,因为考试和实际分析都绕不开。
举个例子:某机 MAR 是 16 位,MDR 是 32 位,那么存储单元个数是 2^16 = 65536 = 64K 个,每个单元 32 位,总容量是 64K × 32 位,换算成字节是 64K × 4B = 256KB。这里单位换算是高频失分点:64K × 32 位不等于 64K × 32 字节,一定要先算位数再除以 8 换算成字节。
再补一个常见变体:如果题目说“按字节编址,主存容量 128MB”,求地址线位数。按字节编址意味着每个地址对应一个字节,容量 128MB = 2^7 × 2^20 = 2^27 字节,所以需要 27 位地址线。这类题的关键是分清“按字编址”和“按字节编址”,两者算出来的地址位数差很多。
| 参数 | 决定什么 | 计算关系 |
|---|---|---|
| MAR 位数 N | 存储单元个数 | 单元数 = 2^N |
| MDR 位数 W | 存储字长(每单元位数) | 容量(位)= 2^N × W |
| 编址方式 | 一个地址对应多少位 | 按字节编址时地址数 = 总字节数 |
2.4 总线与 I/O:数据是怎么进出的
总线是这一章里最容易被讲成“常识题”但实际上很关键的部分。按传输内容分,总线有数据总线、地址总线、控制总线三类。数据总线宽度决定一次能并行传多少位,地址总线宽度决定能寻址多大空间,控制总线传递读写命令、中断请求、时钟同步这些信号。
这里有一个非常实用的判断标准:地址总线的位数决定了系统最大可挂载的内存容量。这也是为什么老机器上内存加不上去,不一定是插槽不够,可能是地址线位数被架构锁死了。
输入输出部分,重点是搞懂三种数据传送方式:程序查询方式(CPU 反复问外设好了没,浪费 CPU)、中断方式(外设好了主动通知 CPU)、DMA 方式(外设直接和内存交换数据,不经过 CPU)。DMA 的出现就是为了解决大批量数据传输时 CPU 被反复打断的问题,比如磁盘读写、网卡收包。你在写高性能网络程序时听到的“零拷贝”,思想源头就在这里——想办法减少数据在部件之间的搬动次数。
提示:I/O 接口通常包含数据寄存器、状态寄存器和控制寄存器三类,编程时你“读写设备”实际上就是读写这几个寄存器,这些寄存器被映射到内存地址或者独立的 I/O 端口空间。
3. 一条指令走完全程:取指到执行的实操推演
3.1 取指周期的微操作,必须能默写
这一节我建议你拿张纸,跟着推一遍,比看十遍书有用。取指周期是所有指令都一样的部分,标准微操作序列如下:
(PC) → MAR,把下一条指令的地址送到地址寄存器M(MAR) → MDR,控制器发读命令,存储器把对应单元内容送到数据寄存器(MDR) → IR,指令送入指令寄存器(PC) + 1 → PC,PC 指向下一条指令(变长指令集里这里是加指令长度)OP(IR) → CU,把操作码部分送到控制单元译码
这五步里有三个地方最容易写错。第一,MDR 到 IR 不是自动的,需要控制信号触发;第二,PC 自增和指令读出是并行的可能,取决于具体设计,但教材通常按顺序写;第三,取指阶段不区分指令类型,所有指令走同一套流程,这也是流水线能成立的前提。
把这几步和“冯·诺依曼瓶颈”联系起来看就很有意思了:取指要占用总线一次,如果执行阶段还要访问内存,那就是第二次。同样长度的时间里,CPU 可能一半时间在等内存。Cache 的存在就是为了让第二次、第三次访问尽量落在快存储里。
3.2 间接寻址与执行周期:数据流的第二种形态
取指之后,如果指令采用间接寻址,还要加一个间址周期:把指令中的形式地址送到 MAR,读存储器得到有效地址,再用这个有效地址去访问真正的数据。这一步的意义是扩大寻址范围——形式地址位数有限,但通过一次间接,可以指向整个地址空间。
执行周期则完全取决于指令类型。以加法指令ADD X(含义是把累加器内容和地址 X 中的内容相加,结果放回累加器)为例:
Ad(IR) → MAR,把指令里的地址码送到 MARM(MAR) → MDR,读出操作数(ACC) + (MDR) → ACC,ALU 执行加法,结果回写累加器
如果是存数指令STA X,则是(ACC) → MDR,再(MDR) → M(MAR)。你会发现规律:所有指令的执行周期都是“搬数据到寄存器—ALU 运算—结果写回”这三个动作的组合变形。把这条规律吃透,你面对任何一条陌生指令都能推出来。
3.3 完整案例:加法指令的逐拍数据流表
下面这张表是我自己复习时整理的,把ADD X指令从取指到执行的每个节拍列清楚,你可以照着这个格式默画一遍。
| 阶段 | 微操作 | 数据流向 | 控制信号要点 |
|---|---|---|---|
| 取指 | (PC) → MAR | PC 到地址寄存器 | PC 输出使能 |
| 取指 | M(MAR) → MDR | 存储器到数据寄存器 | 存储器读 |
| 取指 | (MDR) → IR | 数据寄存器到指令寄存器 | IR 输入使能 |
| 取指 | (PC) + 1 → PC | PC 自增 | PC 计数使能 |
| 执行 | Ad(IR) → MAR | 地址码到地址寄存器 | IR 地址段输出 |
| 执行 | M(MAR) → MDR | 取操作数 | 存储器读 |
| 执行 | (ACC) + (MDR) → ACC | 加法并回写 | ALU 加、ACC 写入 |
这张表的价值不在于背,而在于当你画数据通路图时,每一条线都能对应到表里的一行。我在做单周期 CPU 实验时,就是先把这张表列全,再把它翻译成控制信号,最后才写代码,省了至少一半的调试时间。
心得:如果你在纸上推不出某条指令的微操作序列,八成是因为你不清楚某个寄存器的输入输出由谁控制。回到数据通路图,把每条线的源和目的标出来,问题自然就清楚了。
4. 性能计算:主频、CPI、MIPS 与实际执行时间
4.1 三个公式的推导关系,别死记硬背
性能指标这块,考试必考,实际工作中判断“这个优化值不值”也要用。核心只有一个公式,剩下的都是它的变形:
CPU 执行时间 = 指令条数 × CPI × 时钟周期 = 指令条数 × CPI ÷ 主频
其中 CPI 是每条指令平均需要的时钟周期数。这个公式能推出来另两个:
- MIPS = 主频 ÷ (CPI × 10^6),表示每秒执行多少百万条指令
- MIPS = 指令条数 ÷ (执行时间 × 10^6)
理解这个公式的关键在于:性能由三个因素共同决定,改善其中一个可能让另一个变差。比如精简指令集减少了单条指令的复杂度从而降低 CPI,但可能增加指令条数;提高主频会缩短时钟周期,但可能因为流水线加深而增加 CPI。这种“按下葫芦浮起瓢”的关系,就是体系结构设计的核心张力。
4.2 三道典型题的完整演算过程
第一题(基础型):某 CPU 主频 1GHz,程序共 10^8 条指令,平均 CPI 为 1.5,求执行时间。
时钟周期 = 1 ÷ 10^9 = 1ns。执行时间 = 10^8 × 1.5 × 1ns = 1.5 × 10^8 ns = 0.15s。
第二题(MIPS 型):承上,求该程序运行时的 MIPS。
MIPS = 主频 ÷ (CPI × 10^6) = 1000MHz ÷ 1.5 ≈ 666.7。注意这里单位要统一,主频用 MHz 表示时除以 10^6 才是 MIPS。
第三题(方案对比型,最像真题):同一程序用两个编译器编译,A 方案产生 1.0×10^9 条指令、平均 CPI 为 1.2;B 方案产生 1.2×10^9 条指令、平均 CPI 为 0.8。机器主频 800MHz,问哪个方案快,快多少。
A 方案:1.0×10^9 × 1.2 ÷ 800×10^6 = 1.2×10^9 ÷ 8×10^8 = 1.5s
B 方案:1.2×10^9 × 0.8 ÷ 800×10^6 = 9.6×10^8 ÷ 8×10^8 = 1.2s
B 更快,加速比 = 1.5 ÷ 1.2 = 1.25。
注意:MIPS 是不能跨指令集直接比较的。因为不同指令集完成同一件事需要的指令条数不同,MIPS 高的机器不一定跑得快。这个坑在很多选择题里专门设套,我自己第一次考就栽过。
4.3 加法器进位链:串行进位、组间串行与先行进位
这部分对应热词里的“组间串行进位”,属于运算器设计的核心难点,也是计算题和设计题的重灾区。先把基础说清楚:一位全加器有三个输入(Ai、Bi、低位进位 Ci)和两个输出(和 Si、向高位进位 Ci+1),逻辑表达式是:
Si = Ai ⊕ Bi ⊕ Ci Ci+1 = Ai·Bi + (Ai ⊕ Bi)·Ci为了简化,定义两个中间量:进位产生函数 Gi = Ai·Bi(本位一定产生进位),进位传递函数 Pi = Ai ⊕ Bi(进位能否穿过本位)。于是进位表达式变成Ci+1 = Gi + Pi·Ci,这个形式是后面所有技巧的基础。
**串行进位(行波进位)**是把 n 个全加器直接串起来,低位算出的进位送给高位。结构最简单,但延迟随位数线性增长——16 位就要等进位一路爬过 16 级,速度最慢。这是最朴素的方案,硬件成本最低。
组内并行、组间串行是折中方案,也是热词里明确提到的那个。做法是把 16 位分成 4 组、每组 4 位,组内用先行进位(超前进位),让 4 位以内的进位并行算出,延迟大幅缩短;组与组之间仍然串行传递进位。这样硬件复杂度只增加有限,速度却能明显改善。
组内先行的核心是把进位展开成不依赖低位进位的表达式,比如 4 位一组的组内进位:
C1 = G0 + P0·C0 C2 = G1 + P1·G0 + P1·P0·C0 C3 = G2 + P2·G1 + P2·P1·G0 + P2·P1·P0·C0 C4 = G3 + P3·G2 + P3·P2·G1 + P3·P2·P1·G0 + P3·P2·P1·P0·C0同时为组间串行准备两个组信号:组进位产生函数 G= G3 + P3·G2 + P3·P2·G1 + P3·P2·P1·G0*,组进位传递函数 P= P3·P2·P1·P0*。有了这两个信号,组间就可以用C4 = G* + P*·C0的形式传递,不必逐位等待。
**两级先行进位(组内并行、组间也并行)**是更进一步的做法,组间也用一套先行进位逻辑,速度最快但电路最复杂。三种方案的对比大致如下:
| 方案 | 进位延迟趋势 | 硬件复杂度 | 适用场景 |
|---|---|---|---|
| 串行进位 | 随位数线性增长 | 最低 | 位数少、对速度要求低 |
| 组内并行、组间串行 | 明显低于串行,组数线性 | 中等 | 16/32 位通用加法器常用 |
| 两级先行进位 | 接近对数级增长 | 最高 | 高性能运算部件 |
关于延迟的估算,我必须提醒一句:具体数值完全取决于教材假设的门延迟标准。有的教材假设一级与门或或门为 1ty、异或门为 3ty,有的直接给出门级数。所以做题时一定要看题目给的假设,不要套用记忆里的固定数字。我见过太多人背着“16 位串行要 32 个门延迟”去考试,结果题目假设不同,全错。
5. 实验与调试:那些文档里不写的坑
5.1 从零搭一台单周期 CPU 的最小可行路径
实验课配的那本使用手册通常把步骤写得很正规,但真正动手时你会发现最难的是“顺序”。我推荐这条路径,亲测比盲目按手册走省时间:
先定指令集。不要贪多,先支持四五条就够了:取数、存数、加法、无条件跳转、条件跳转。指令格式定下来,后面所有设计才有依据。
再画数据通路。把 PC、IR、寄存器堆、ALU、存储器都摆上去,然后照着 3.3 节那张表把每条线连起来。这一步不要急着写代码,画到你能在纸上推完一条 ADD 指令为止。
接着列控制信号表。每条指令在每个阶段需要哪些信号置 1,全部列成表格。这张表就是你写控制器代码的直接依据。
最后才是写代码、仿真、上板。这个顺序看起来慢,实际上能避免“改一处崩一片”的反复折腾。我见过有同学上来就写 Verilog,写到一半发现指令格式设计有冲突,只能推倒重来,两天白干。
5.2 信号为未知值时的排查顺序
仿真时最常见的现象是:波形里某个信号一直是未知状态,或者电路里某根线颜色不对(工具里浮动值和冲突通常会用特殊颜色标出,蓝线、红线都值得警惕)。这类问题的排查有一套固定顺序,我总结成表:
| 现象 | 优先排查 | 常见原因 |
|---|---|---|
| 某信号始终为未知 | 该信号的驱动源 | 寄存器未复位、模块未例化、端口未连接 |
| PC 不递增 | 时钟与复位 | 时钟未翻转、复位信号常有效 |
| 存储器读出全 0 | 地址与读使能 | 地址越界、读信号未拉高 |
| 输出偶尔正确偶尔错 | 时序竞争 | 组合逻辑环路、未同步的异步输入 |
| 仿真对但上板错 | 约束与时钟 | 管脚约束错误、时钟频率过高 |
排查时有个非常实用的原则:从信号源头往末端追,而不是从末端反推。先确认时钟在跳、复位有效后释放,再看 PC 有没有动,再看 MAR 有没有拿到地址。按这个链条走,九成问题能在十分钟内定位。
提示:组合逻辑环路是初学者最容易制造也最难发现的错误。特征是仿真时信号保持某个值不变或者出现无法解释的振荡。检查方法很简单:确认每个信号都有明确的驱动源,且不依赖自己的输出。
5.3 硬件描述代码里的几个典型陷阱
写这类代码和写软件最大的区别是它是并行的,很多在软件里理所当然的写法在硬件里完全不是那么回事。挑几个我踩过或者看别人踩过的坑说说。
第一个是不完整的条件分支导致锁存器。在组合逻辑的 always 块里,如果 if 没有 else、case 没有 default,综合工具会推断出一个锁存器来“保持原值”,这不是你想要的行为,还会带来时序问题。习惯是:组合逻辑里把所有分支写全,或者给输出赋默认值。
第二个是阻塞赋值和非阻塞赋值混用。时序逻辑里用非阻塞赋值,组合逻辑里用阻塞赋值,这是基本纪律。混用的后果是仿真结果和综合结果不一致,而且这种 bug 极难定位,因为仿真看起来是对的。
第三个是复位策略不统一。有的寄存器用同步复位、有的用异步复位,混在一起时,复位释放的瞬间容易出现亚稳态。简单的做法是一门课里统一种复位方式,别自作聪明。
第四个是位宽不匹配。给一个 8 位端口接了个 4 位信号,工具可能只给个警告,但行为可能是高位补零也可能是截断,具体取决于上下文。这种问题在波形上表现为“数据莫名其妙少了一半”,非常隐蔽。我的习惯是每次综合后把所有位宽警告全部看完,一个都不放过。
下面是一段简化的寄存器堆写端口代码,可以感受一下风格:
always @(posedge clk or posedge rst) begin if (rst) begin for (i = 0; i < 32; i = i + 1) regs[i] <= 32'b0; end else if (we && waddr != 5'b0) begin regs[waddr] <= wdata; end end assign rdata_a = (raddr_a == 5'b0) ? 32'b0 : regs[raddr_a]; assign rdata_b = (raddr_b == 5'b0) ? 32'b0 : regs[raddr_b];这段代码里有两个细节值得注意:一是零号寄存器被强制为 0,这是很多指令集的约定,读出来恒为零,写进去被丢弃;二是读操作写成组合逻辑,这样在同一个时钟周期内就能读出数据,不需要额外等待。这两点如果不注意,跑程序时会莫名其妙算错。
6. 常见问题、复习策略与往后的延伸
6.1 概念易混对照表,考前扫一遍
这门课的概念密度大,很多词看着像、用着不一样。我把最容易混的几组整理成表,考前一天扫一遍,效率比重新翻书高。
| 易混概念 | 区别要点 |
|---|---|
| 时钟周期 / 机器周期 / 指令周期 | 依次由小到大,机器周期由若干时钟周期组成,指令周期由若干机器周期组成 |
| MAR / MDR | MAR 决定寻址范围,MDR 决定存储字长 |
| MIPS / CPI | MIPS 越高通常性能越好,但跨指令集不可比;CPI 是每指令周期数,越低越好 |
| 中断 / DMA | 中断需要 CPU 参与搬运,DMA 由专门控制器完成,CPU 只负责启动和收尾 |
| 串行进位 / 先行进位 | 前者延迟线性增长,后者用更多逻辑换速度 |
| 硬布线 / 微程序控制器 | 前者快但难改,后者灵活但慢 |
6.2 问答题怎么答到点上
考试里的问答题,评分标准往往不是看你写多少字,而是看你有没有踩中得分点。我的经验是把答案组织成“定义 + 作用 + 为什么这样设计”三层。
比如问“为什么现代计算机采用以存储器为中心的结构”,只答“因为效率高”拿不到分。要答:原始结构以运算器为中心,输入输出数据需要经过运算器中转,运算器成为瓶颈;改为以存储器为中心后,各部件之间可以经总线直接交换数据,提高了并行性和吞吐能力;这也是后续引入总线结构和 DMA 的结构基础。
再比如问“总线宽度对性能的影响”,答“地址总线决定可寻址空间,数据总线决定单次传输的数据量,两者共同影响系统最大内存容量和数据传输带宽”就够了,如果再加一句“数据总线宽度是决定带宽的关键因素,但受限于芯片引脚数和成本”,就能拉开差距。
6.3 从这一章往后延伸:相关性、局部性与后续章节
学完基本组成,后面几章的伏笔其实都已经埋好了。存储层次结构会在“局部性原理”上展开:时间局部性说刚访问过的数据很可能再被访问,空间局部性说相邻数据很可能被一起访问,Cache 的设计全部建立在这两条假设上。你写代码时把热数据集中放置、按顺序访问,就是在迎合硬件。
流水线部分会讲三类相关性,这是这一章知识的直接延伸。结构相关是硬件资源冲突,比如取指和访存同时要用存储器;数据相关是后面的指令要用前面指令还没写回的结果,细分下来有写后读、读后写、写后写三种;控制相关是转移指令导致下一条指令地址不确定。解决手段分别是增加资源、数据旁路和前递、分支预测。这三类相关性不是背的,而是从“单周期数据通路”自然生长出来的问题。
想往后走的同学,我建议的路线是:先把这一章的数据通路彻底画熟,再学单周期 CPU 实现,然后进流水线,最后碰 Cache 和虚拟存储。每一步都建立在前一步的数据流图上,跳步会很痛苦。
最后分享一个我自己复习时的笨办法:拿一张 A3 纸,把整个数据通路画一遍,然后把所有指令的微操作序列写在旁边,再用不同颜色的笔标出每条线的控制信号来源。画完之后你会发现,这一章的知识点其实只有一页纸的量,剩下的都是它的展开。我前后画过三遍,第一遍花了四个小时,第二遍一小时,第三遍二十分钟。这个从慢到快的过程,就是真正学进去的标志。