简介:《计算机系统结构(张晨曦版)》课后习题参考答案以doc文档格式整理,面向计算机专业本科生、考研复习者及自学该课程的读者,帮助逐题核对知识点与解题过程。压缩包内包含1个doc文件,整体约170KB,内容聚焦教材第1章“计算机系统结构的基本概念”,覆盖层次结构、虚拟机与翻译/解释、计算机组成与实现、Amdahl定律、程序局部性原理、CPI、Flynn分类法、并行性等级等核心术语的释义,并给出主存系统设计实例、定量原理说明及一道主频400MHz机器的CPI、MIPS与执行时间计算题的完整解答,可作为课堂学习与期末备考的辅助资料。资料包目前已有1379人学习使用,适合同步对照教材章节查漏补缺,强化对系统结构基础概念和计算方法的掌握。
1. 为什么要把这份课后答案当作性能工程的模型库来读
很多人拿到张晨曦版《计算机系统结构》的课后答案,第一反应是照着抄完作业就扔了。但如果你做过几年性能优化,会意识到这份材料里的习题其实是麻雀虽小五脏俱全:从 Amdahl 定律到 CPI 计算,从流水线冲突到向量链接,几乎每个题都能映射到真实 CPU 设计或业务系统调优中的决策点。我自己的习惯是,拿到一份课后题先不看答案,而是把每道题当作一个“系统参数已知,求性能边界”的小实验,这比背概念有用得多。这份答案的独特之处在于它把术语定义和量化计算放在一起,比如 1.6 题直接给你指令分布让你算有效 CPI,1.8 题又让你反过来解可改进比例,这种“正推 + 反推”的练习恰好是性能工程里最常用的能力。适合三类人:正在复习考研 408 或计算机统考的在校生,准备体系结构面试的工程师,以及需要快速回顾性能模型的产品或运维同学。下面我按自己的拆解路径,把这份答案里的核心知识点重构成能直接上手的分析工具。
2. 从术语表到量化模型:先搭建系统结构的概念坐标系
2.1 翻译、解释、模拟、仿真:四个执行模型不能混
很多初学者把“翻译”和“解释”当成一回事,又把“模拟”和“仿真”混着说。课后答案 1.1 给出的定义其实非常精确,区别在于“转换发生的时间”和“实现载体”。我把它们整理成表:
| 执行模型 | 实现媒介 | 典型场景 | 关键特征 |
|---|---|---|---|
| 翻译 | 转换程序 | 编译器把高级语言转成机器码 | 先整体转换,再执行低一级程序 |
| 解释 | 低一级等效程序 | Python/Java 解释器逐条执行 | 每取一条指令,就转去执行一段低一级程序 |
| 模拟 | 宿主机上的软件 | QEMU 模拟另一套指令集 | 用软件方法在宿主机上实现虚拟机指令系统 |
| 仿真 | 宿主机微程序 | 硬件仿真器验证芯片设计 | 用微程序解释目标机指令系统,速度更快但依赖硬件 |
翻译的核心是一次性换档,解释是边翻译边执行。模拟和仿真的区别更微妙:模拟是“软件包一层”,不要求宿主机硬件支持目标指令;仿真则要求宿主机的微程序能直接解释目标指令,所以仿真通常出现在硬件验证阶段,速度比模拟快一个量级。理解这四个词,后面看“虚拟机”“兼容机”才不会乱。
2.2 并行性分级与 Flynn 分类法:先知道机器属于哪一类
从数据处理角度,并行性从低到高分成四档:字串位串、字串位并、字并位串、全并行。从执行程序角度来看又分成五档:指令内部并行、指令级并行、线程级并行、任务级并行、作业级并行。这两套体系容易混淆,我一般这样记:数据处理分级看“一个时钟周期里同时处理多少位、多少字”,执行程序分级看“同时执行多少条指令、多少个线程、多少个任务”。
Flynn 分类法则是按指令流与数据流的多倍性来切:
| 类别 | 指令流 | 数据流 | 实际例子 |
|---|---|---|---|
| SISD | 1 | 1 | 经典单核 CPU |
| SIMD | 1 | 多 | GPU、Intel AVX 向量指令 |
| MISD | 多 | 1 | 几乎没有商业实现 |
| MIMD | 多 | 多 | 多核服务器、分布式集群 |
课后答案 1.3 只要求分类,但实际工作中这个分类特别有用。比如你看到某个算法说“适合 SIMD”,本质上是它的数据并行度高、控制逻辑简单,可以一股脑喂给向量计算单元;而一个多线程服务是 MIMD,因为每个核心跑不同的控制流。
2.3 CPI 与 MIPS:用一段 Python 复现课后 1.6 的计算
课后 1.6 给了典型的指令分布,要求算有效 CPI、MIPS 和程序执行时间。这种题就是送分题,但单位容易写错。我用 Python 复现一遍:
# 习题 1.6 数据:指令执行数量和平均 CPI inst_count = [45000, 75000, 8000, 1500] inst_cpi = [1, 2, 4, 2] total = sum(inst_count) # 总指令数 = 129500 total_cycles = sum(c * n for c, n in zip(inst_cpi, inst_count)) cpi = total_cycles / total # 有效 CPI = 1.776 freq_hz = 400e6 # 主频 400MHz mips = (freq_hz / 1e6) / cpi # MIPS = 225.225 exec_time_s = total_cycles / freq_hz # 单位:秒 print(f"有效 CPI = {cpi:.3f}") print(f"MIPS = {mips:.3f}") print(f"程序执行时间 = {exec_time_s * 1e6:.3f} us")代码里total_cycles是所有指令的时钟周期总和,exec_time_s用周期总数除以主频得到秒,再换算成微秒。输出结果:有效 CPI 1.776,MIPS 225.225,程序执行时间 575 us。
注意原答案第 1.6 题写的是“575s”,这里明显缺了单位换算,正确结果是 575 微秒。这种小坑在课后答案里很常见,所以不能只背结论,要亲手验算一遍。MIPS 的定义是每秒执行多少百万条指令,因此计算时要除以1e6,而不是直接用 MHz 数值去除 CPI。
3. Amdahl 定律的实战推演:从课后 1.7 到 1.8 学会做性能预算
3.1 Amdahl 定律的两种写法与参数含义
Amdahl 定律是系统结构里最容易被误解的公式。课后 1.7 说的是:某一功能处理速度加快 10 倍,该功能处理时间占总运行时间 40%,求系统整体加速比。
公式的标准形式是:
加速比 Sn = 1 / ( (1 - Fe) + Fe / Se )其中Fe是可改进部分占总执行时间的比例,Se是这部分改进后的加速比。这里的关键是Fe是时间比例,不是代码行数比例。很多人把“优化了 40% 的代码”当成Fe=0.4,这没问题,但如果你优化的是所有代码里的一条热点路径,Fe应该用 profiler 测出来的时间占比,而不是代码行数占比。
把 1.7 的数据代入:Fe=0.4,Se=10,加速比= 1 / (0.6 + 0.4/10) = 1.5625,也就是性能提升为原来的 1.5625 倍。
3.2 多部件优化:如何用扩展公式解 1.8(1)
1.8 题升级了:三个部件可改进,加速比分别为 30、20、10,已知前两个可改进比例都是 30%,问第三个可改进比例达到多少时,系统加速比才能到 10。
这个场景在真实性能优化里太常见了:你手里有几个优化手段,每个手段的收益和成本不同,要反推某个手段的成本是否值得做。扩展 Amdahl 公式是:
Sn = 1 / ( (1 - ΣFi) + Σ(Fi / Si) )其中ΣFi是所有可改进部分的时间占比总和,Σ(Fi/Si)是改进后剩下时间占比之和。我把求解代码写出来,用符号计算避免手算解方程出错:
from sympy import symbols, Eq, solve F3 = symbols('F3') S1, S2, S3 = 30, 20, 10 F1, F2 = 0.3, 0.3 Sn_target = 10 # 扩展 Amdahl:分母 = 不可改进部分 + 各部件改进后时间占比 denominator = (1 - F1 - F2 - F3) + (F1 / S1 + F2 / S2 + F3 / S3) eq = Eq(1 / denominator, Sn_target) print(solve(eq, F3))运行结果F3 = 0.36,也就是部件 3 的可改进比例要达到 36%。这里1 - F1 - F2 - F3是三个部件之外不可改进部分的时间占比,改进前后都不变;代价是它把分母压住,限制整体加速比上限。
3.3 不可改进部分占比:1.8(2) 的手算技巧
1.8 第二问换了条件:三个部件可改进比例分别为 30%、30%、20%,加速比分别为 30、20、10,问改进后不可加速部分的执行时间在总执行时间中占多少。
设改进前总执行时间为T,三个部件改进前总占用0.8T,不可改进部分是0.2T。改进后三个部件的执行时间变成:
| 部件 | 改进前占比 | 改进后执行时间 |
|---|---|---|
| 部件 1 | 0.3T | 0.3T / 30 = 0.01T |
| 部件 2 | 0.3T | 0.3T / 20 = 0.015T |
| 部件 3 | 0.2T | 0.2T / 10 = 0.02T |
| 不可加速部分 | 0.2T | 0.2T |
所以总时间Tn = 0.01T + 0.015T + 0.02T + 0.2T = 0.245T,不可加速部分占比0.2 / 0.245 = 0.8163。
这个结果很反直觉:三个部件都优化了,但最终系统里 81.63% 的时间花在了“没优化”的部分。这正是 Amdahl 定律给性能工程师的警告:如果热点只分散在多个小模块里,逐个优化每个模块的收益会迅速衰减,这时候应该考虑能不能合并优化路径,而不是继续抠单个部件。
4. 指令集与流水线:把课后题变成 CPU 设计的决策练习
4.1 用 CPU 性能公式拆解 CISC 和 RISC 的取舍
课后 2.11 提到 CPU 性能公式:CPU 时间 = IC × CPI × T。这个公式是评价指令集结构的万能标尺。CISC 的目标代码紧凑,IC 小,但指令复杂导致 CPI 大、周期时间长;RISC 指令简单,CPI 小,但同样功能需要更多指令,IC 大。
我列一个假设的对比表:
| 指标 | CISC | RISC |
|---|---|---|
| 指令条数 IC | 100 | 120 |
| 平均 CPI | 4 | 2 |
| 时钟周期 T | 1ns | 1ns |
| CPU 时间 | 400ns | 240ns |
这个例子说明 RISC 即使多执行 20% 的指令,只要 CPI 降一半,整体仍然更优。这也是为什么现代芯片普遍采用类 RISC 思想,即使 x86 也要在内部翻译成微操作来降低等效 CPI。
4.2 流水线冲突与定向:手动推演 3.16 的三代方案
课后 3.16 是流水线章节的分水岭,它给了一个 MIPS 循环,要求分析不同优化策略下的执行周期。原代码是:
LOOP: LW R1, 0(R2) DADDIU R1, R1, #1 SW R1, 0(R2) DADDIU R2, R2, #4 DSUB R4, R3, R2 BNEZ R4, LOOP第一种情况:没有任何定向硬件,分支用排空策略。每个 LW 之后要等 R1 写回,DADDIU 要用 R1,产生写后读冲突;SW 又依赖 DADDIU 的加一结果。没有定向时,相关指令之间必须插入 stall,每个分支还要清空三段。答案给出每次迭代占 17 个周期,99 次迭代加尾部共 1684 个周期。这个数字不需要背,但你要能画出来:问题出在每条指令都要等前一条写寄存器完成。
第二种情况:有正常定向路径,分支预测失败。定向把 ALU 结果直接送到后续指令的输入端,写后读冲突不再需要停顿。但分支仍在第三周期才能解析,所以循环的每次迭代缩短到 10 周期,总周期 991。
第三种情况:有定向路径 + 单周期延迟分支,这时可以重排指令。答案给了一个经典调度:
LOOP: LW R1, 0(R2) DADDIU R2, R2, #4 DADDIU R1, R1, #1 DSUB R4, R3, R2 BNEZ R4, LOOP SW R1, -4(R2)为什么这么排:把DADDIU R2提前,让R2的更新早于DSUB,消除 DSUB 对 R2 的写后读相关;SW挪到分支指令的延迟槽里,这是利用延迟分支的“无论是否跳转都执行延迟槽指令”语义。因为SW原本在DADDIU R1之后,现在R1已经加一,所以存储地址改为-4(R2),而R2已经加 4,正好指向原地址。每次迭代从 17 周期压到 6 周期,总周期 598。
这个调度在真实编译器里叫做“指令调度”(instruction scheduling)和“延迟槽填充”(delay slot filling)。现在流水线越做越深,延迟槽机制已经被动态调度取代,但手工调度的思路仍然是理解 Tomasulo 算法和乱序执行的基础。
4.3 静态多功能流水线调度:从 3.14 看功能部件复用
3.14 是一道静态多功能流水线题:五个功能段,加法用 1、3、4、5 段,乘法用 1、2、5 段,第 3 段时间 2Δt,其余都是 Δt。要算(A1+B1)×(A2+B2)+...这类表达式,关键不在套公式,而在选择计算顺序。
最优顺序是先把四个加法算完,再做两个乘法,最后加总。为什么?因为如果先算乘法,乘法的第 2 段会占用加法要用的第 3 段,静态流水线同一时间只能连接成一种功能,切换功能要等排空。把所有加法集中做完,流水线只切换一次。这个思想对应现在处理器里的“功能部件共享”和“功耗墙”优化:硬件功能单元越少,越要考虑任务发射顺序,否则流水线频繁排空,吞吐率掉得厉害。
5. 用表格和脚本自检:把课后答案变成可复用的验证工具
5.1 建立概念-计算-场景映射表
这份课后答案的目录是按章节走的,但复习时最好按“概念 → 公式 → 应用”重新组织。我做了个映射表,每看完一章就往里填对应题号和真实场景:
| 概念 / 公式 | 课后题号 | 真实应用场景 |
|---|---|---|
| 层次结构 / 虚拟机 | 1.1 | 语言运行时设计、JVM 分层 |
| CPI / MIPS | 1.6 | Benchmark 分析、性能报告解读 |
| Amdahl 定律 | 1.7 / 1.8 | 容量规划、优化 ROI 评估 |
| CISC vs RISC | 2.6 / 2.7 / 2.11 | CPU 架构选型、编译器后端设计 |
| 流水线冲突与定向 | 3.4 / 3.16 | 处理器级流水线设计、编译器调度 |
| 向量链接 / 半性能向量长度 | 3.18 / 3.19 | 向量处理器/GPU 性能预测 |
这张表最大的作用不是背诵,而是帮你把每个公式和它背后的决策场景绑在一起。比如你在优化一个数据库服务,看到查询延迟高,第一反应是看Fe是哪个模块的耗时占比,再套 Amdahl 定律判断优化上限,而不是盲目加机器。
5.2 把常用公式封装成 Python 函数
我建议把这份答案里的高频公式写成一个arch_utils.py脚本,方便以后做题或估算:
def amdahl(fe, se): """单部件 Amdahl 定律:fe 可改进时间占比,se 部件加速比""" return 1 / ((1 - fe) + fe / se) def multi_amdahl(parts): """多部件扩展 Amdahl:parts 是 (fe, se) 列表""" fe_sum = sum(fe for fe, se in parts) improved_sum = sum(fe / se for fe, se in parts) return 1 / (1 - fe_sum + improved_sum) def effective_cpi(counts, cpis): """有效 CPI:counts 是指令数量列表,cpis 是每类指令平均周期数""" return sum(c * n for c, n in zip(cpis, counts)) / sum(counts) def exec_time(total_cycles, freq_hz): """总周期数转执行时间(秒)""" return total_cycles / freq_hz # 用课后题做断言自检 assert abs(amdahl(0.4, 10) - 1.5625) < 1e-6 assert abs(effective_cpi([45000, 75000, 8000, 1500], [1, 2, 4, 2]) - 1.776) < 1e-3 assert abs(multi_amdahl([(0.3, 30), (0.3, 20), (0.36, 10)]) - 10) < 1e-6 print("all assertions passed")这几个函数把课后题的核心计算逻辑抽出来了。以后遇到新的性能数据,不用再翻公式,直接调用。assert的作用是防止你抄错答案后还不自知,我用 1.7、1.6、1.8 的已知结论做了三个断言,能跑通就说明函数实现和手算一致。
5.3 复习时的快速定位法
如果你是为了备考,建议用“错题三连问”来定位薄弱点:第一步,这题错了是公式不熟还是概念不清?第二步,把错题对应的章节名写下来,比如“流水线冲突”或“向量处理”;第三步,用上文那个映射表倒着找同类题,连续做三道同类题。这样比从头到尾刷一遍答案效率高很多。另外,我习惯用 Git 记录复习进度,比如每次更新这个arch_utils.py都提交一次,并把错题原因写在 commit message 里,回滚复习时直接看错误历史。
本文还有配套的精品资源,点击获取