
“计算机组成原理”这四个字在课程表上看着平平无奇但它属于那种“学的时候觉得抽象、用起来发现真香”的课。而“计算机的基本组成”又是整门课的地基——运算器、控制器、存储器、输入输出设备加上把它们串起来的总线这几块东西怎么分工、怎么配合、一条指令怎么从内存被搬到 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^NMDR 位数 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 → PCPC 指向下一条指令变长指令集里这里是加指令长度OP(IR) → CU把操作码部分送到控制单元译码这五步里有三个地方最容易写错。第一MDR 到 IR 不是自动的需要控制信号触发第二PC 自增和指令读出是并行的可能取决于具体设计但教材通常按顺序写第三取指阶段不区分指令类型所有指令走同一套流程这也是流水线能成立的前提。把这几步和“冯·诺依曼瓶颈”联系起来看就很有意思了取指要占用总线一次如果执行阶段还要访问内存那就是第二次。同样长度的时间里CPU 可能一半时间在等内存。Cache 的存在就是为了让第二次、第三次访问尽量落在快存储里。3.2 间接寻址与执行周期数据流的第二种形态取指之后如果指令采用间接寻址还要加一个间址周期把指令中的形式地址送到 MAR读存储器得到有效地址再用这个有效地址去访问真正的数据。这一步的意义是扩大寻址范围——形式地址位数有限但通过一次间接可以指向整个地址空间。执行周期则完全取决于指令类型。以加法指令ADD X含义是把累加器内容和地址 X 中的内容相加结果放回累加器为例Ad(IR) → MAR把指令里的地址码送到 MARM(MAR) → MDR读出操作数(ACC) (MDR) → ACCALU 执行加法结果回写累加器如果是存数指令STA X则是(ACC) → MDR再(MDR) → M(MAR)。你会发现规律所有指令的执行周期都是“搬数据到寄存器—ALU 运算—结果写回”这三个动作的组合变形。把这条规律吃透你面对任何一条陌生指令都能推出来。3.3 完整案例加法指令的逐拍数据流表下面这张表是我自己复习时整理的把ADD X指令从取指到执行的每个节拍列清楚你可以照着这个格式默画一遍。阶段微操作数据流向控制信号要点取指(PC) → MARPC 到地址寄存器PC 输出使能取指M(MAR) → MDR存储器到数据寄存器存储器读取指(MDR) → IR数据寄存器到指令寄存器IR 输入使能取指(PC) 1 → PCPC 自增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.2B 方案产生 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.5sB 方案1.2×10^9 × 0.8 ÷ 800×10^6 9.6×10^8 ÷ 8×10^8 1.2sB 更快加速比 1.5 ÷ 1.2 1.25。注意MIPS 是不能跨指令集直接比较的。因为不同指令集完成同一件事需要的指令条数不同MIPS 高的机器不一定跑得快。这个坑在很多选择题里专门设套我自己第一次考就栽过。4.3 加法器进位链串行进位、组间串行与先行进位这部分对应热词里的“组间串行进位”属于运算器设计的核心难点也是计算题和设计题的重灾区。先把基础说清楚一位全加器有三个输入Ai、Bi、低位进位 Ci和两个输出和 Si、向高位进位 Ci1逻辑表达式是Si Ai ⊕ Bi ⊕ Ci Ci1 Ai·Bi (Ai ⊕ Bi)·Ci为了简化定义两个中间量进位产生函数 Gi Ai·Bi本位一定产生进位进位传递函数 Pi Ai ⊕ Bi进位能否穿过本位。于是进位表达式变成Ci1 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] 32b0; end else if (we waddr ! 5b0) begin regs[waddr] wdata; end end assign rdata_a (raddr_a 5b0) ? 32b0 : regs[raddr_a]; assign rdata_b (raddr_b 5b0) ? 32b0 : regs[raddr_b];这段代码里有两个细节值得注意一是零号寄存器被强制为 0这是很多指令集的约定读出来恒为零写进去被丢弃二是读操作写成组合逻辑这样在同一个时钟周期内就能读出数据不需要额外等待。这两点如果不注意跑程序时会莫名其妙算错。6. 常见问题、复习策略与往后的延伸6.1 概念易混对照表考前扫一遍这门课的概念密度大很多词看着像、用着不一样。我把最容易混的几组整理成表考前一天扫一遍效率比重新翻书高。易混概念区别要点时钟周期 / 机器周期 / 指令周期依次由小到大机器周期由若干时钟周期组成指令周期由若干机器周期组成MAR / MDRMAR 决定寻址范围MDR 决定存储字长MIPS / CPIMIPS 越高通常性能越好但跨指令集不可比CPI 是每指令周期数越低越好中断 / DMA中断需要 CPU 参与搬运DMA 由专门控制器完成CPU 只负责启动和收尾串行进位 / 先行进位前者延迟线性增长后者用更多逻辑换速度硬布线 / 微程序控制器前者快但难改后者灵活但慢6.2 问答题怎么答到点上考试里的问答题评分标准往往不是看你写多少字而是看你有没有踩中得分点。我的经验是把答案组织成“定义 作用 为什么这样设计”三层。比如问“为什么现代计算机采用以存储器为中心的结构”只答“因为效率高”拿不到分。要答原始结构以运算器为中心输入输出数据需要经过运算器中转运算器成为瓶颈改为以存储器为中心后各部件之间可以经总线直接交换数据提高了并行性和吞吐能力这也是后续引入总线结构和 DMA 的结构基础。再比如问“总线宽度对性能的影响”答“地址总线决定可寻址空间数据总线决定单次传输的数据量两者共同影响系统最大内存容量和数据传输带宽”就够了如果再加一句“数据总线宽度是决定带宽的关键因素但受限于芯片引脚数和成本”就能拉开差距。6.3 从这一章往后延伸相关性、局部性与后续章节学完基本组成后面几章的伏笔其实都已经埋好了。存储层次结构会在“局部性原理”上展开时间局部性说刚访问过的数据很可能再被访问空间局部性说相邻数据很可能被一起访问Cache 的设计全部建立在这两条假设上。你写代码时把热数据集中放置、按顺序访问就是在迎合硬件。流水线部分会讲三类相关性这是这一章知识的直接延伸。结构相关是硬件资源冲突比如取指和访存同时要用存储器数据相关是后面的指令要用前面指令还没写回的结果细分下来有写后读、读后写、写后写三种控制相关是转移指令导致下一条指令地址不确定。解决手段分别是增加资源、数据旁路和前递、分支预测。这三类相关性不是背的而是从“单周期数据通路”自然生长出来的问题。想往后走的同学我建议的路线是先把这一章的数据通路彻底画熟再学单周期 CPU 实现然后进流水线最后碰 Cache 和虚拟存储。每一步都建立在前一步的数据流图上跳步会很痛苦。最后分享一个我自己复习时的笨办法拿一张 A3 纸把整个数据通路画一遍然后把所有指令的微操作序列写在旁边再用不同颜色的笔标出每条线的控制信号来源。画完之后你会发现这一章的知识点其实只有一页纸的量剩下的都是它的展开。我前后画过三遍第一遍花了四个小时第二遍一小时第三遍二十分钟。这个从慢到快的过程就是真正学进去的标志。