计算机体系结构 00:一次数组求和经过哪些层

一个程序从源代码变成结果,中间至少经过四层问题。ISA 规定软件能看见的寄存器、指令和内存效果;微架构决定这些指令怎样在一台具体处理器里重叠执行;实现工艺决定电路能以怎样的延迟、功耗和面积工作;系统软件负责把进程、虚拟地址、异常和设备组织成可用环境。把这四层混在一起,最常见的错误是用“CPU 很快”“缓存慢了”“编译器优化了”这种句子解释一切。

本篇只做入口:沿一个数组求和程序,把源代码、指令、硬件可见状态和结果检查分开标注。证据等级为“手算”“功能执行”。这里不声称得到流水线周期、缓存命中率或真实 CPU 的内部状态;第 07 篇的基础执行器只提交 RV32I 子集的架构状态,后续流水线和缓存章节再加入时序模型。

问题:同一个结果来自哪几种机制

示例工作负载是把 5 个 32 位整数相加:

1
2
3
4
5
uint32_t a[5] = {3, 4, 5, 6, 7};
uint32_t sum = 0;
for (size_t i = 0; i < 5; i++) {
sum += a[i];
}

手算结果是 3 + 4 + 5 + 6 + 7 = 25。这个等式说明算法结果,不说明机器怎样得到结果。机器层还要回答:数组元素放在哪里,循环变量放在哪个寄存器,分支怎样回到循环头,最后一次分支为什么不跳转,结果写回哪个内存地址。

本系列采用 RISC-V 作为主线 ISA。RISC-V 官方非特权规范把 RV32I 描述为 32 位基本整数指令集,包含 32 个 32 位整数寄存器,x0 硬连为 0,pc 保存当前指令地址;基本指令格式固定为 32 位,基础 ISA 的指令地址按 4 字节对齐。后续文章会逐项进入编码和 ABI,本篇只使用这些规则解释轨迹。参考资料见文末的 RISC-V 官方规范链接。

模型与边界

本篇的教学程序来自 examples/computer-architecture/base/programs/sum.hex,同名素材目录里保存了 sum.hex.txt。输入格式很窄:.text 每行一个 32 位指令字,.data 每行一个字节地址和 32 位数据。基础执行器按小端字节序装入内存,执行一个有限 RV32I 子集:addilwaddbnesw 等。它不支持系统调用、特权指令、压缩指令、异常恢复、流水线、缓存和真实计数器。

这条边界很重要。执行器输出的是“功能执行”:每条已提交指令改变了哪些架构寄存器或内存。它不能证明商品 CPU 也用了 29 个周期,不能证明分支预测怎样工作,也不能证明缓存是否命中。后续所有章节都沿这个原则扩展:先把可见语义说清楚,再讨论某个实现如何让它更快。

从源代码到指令轨迹

这个程序的数据放在字节地址 128、132、136、140、144,结果写到 148。指令先设置三个寄存器:

寄存器 含义 初值
x1 当前数组元素地址 128
x2 剩余元素个数 5
x3 累加和 0

循环体做 5 件事:从 x1 指向的地址 lw 一个 32 位字到 x4;把 x4 加到 x3;把 x1 加 4 指向下一个元素;把 x2 减 1;若 x2 != x0,用 bne 回到循环头。最后一次 x2 变成 0,分支不再跳转,swx3=25 写到地址 148。

功能轨迹的前几步如下,完整输出在同名素材目录的 sum-trace.jsonl.txt

1
2
3
{"step":0,"pc":0,"inst":"0x08000093","op":"addi","rd":1,"value":128,"imm":128,"next_pc":4,"x0":0}
{"step":1,"pc":4,"inst":"0x00500113","op":"addi","rd":2,"value":5,"imm":5,"next_pc":8,"x0":0}
{"step":2,"pc":8,"inst":"0x00000193","op":"addi","rd":3,"value":0,"imm":0,"next_pc":12,"x0":0}

循环结束处的关键记录是:

1
2
{"step":28,"pc":32,"inst":"0x08302a23","op":"sw","addr":148,"value":25,"next_pc":36,"x0":0}
{"halt":"pc_at_end","steps":29,"x0":0,"x1":148,"x3":25,"mem148":25,"mem160":0}

这两行能支持的结论很有限,但足够清楚:在这个教学 ISA 子集和这份输入上,程序提交 29 条指令后停止,架构寄存器 x3 与内存地址 148 都保存 25。这里没有“可能更快”的判断,只有“可见状态正确”的判断。

同一逻辑也可以用真实 C 程序在本机编译。附件 sum.c 运行结果为 sum-host-output.txt

1
sum5=25

Apple clang 21.0.0 在本机 arm64 上对 sum5 生成的 -O2 汇编见 sum-arm64-O2.s.txt,核心片段是:

1
2
3
4
5
6
7
_sum5:
ldr q0, [x0]
ldr w8, [x0, #16]
addv.4s s0, v0
fmov w9, s0
add w0, w9, w8
ret

这段汇编不是 RISC-V;它是本机 AArch64 产物。它证明同一 C 源程序可以被降到另一套 ISA,且宿主运行结果仍为 25。手写 RV32I 机器码和本机 AArch64 汇编服务于两个不同问题:前者让后续教学执行器有稳定输入,后者让“源代码会变成目标机器指令”这件事可检查。

跨层路径可以这样读:

1
2
3
4
5
6
C 源码 sum5 ──Apple clang -O2──→ AArch64 指令 ──本机执行──→ sum5=25
同一求和算法 ──手写编码──────→ RV32I 指令 ──参考执行器──→ x3=25、mem[148]=25

PC → 取指 → 译码 → 执行 → 更新状态

后续时序模型改变阶段重叠,不改变指令语义
本篇对象 可见证据
源代码 sum.csum5 循环 sum5=25
目标 ISA AArch64 汇编、手写 RV32I 机器码 ldr/addv/add 片段;sum.hex.txt
架构状态 RV32I pcx1/x2/x3/x4、内存地址 148 sum-trace.jsonl.txt
测量边界 只检查结果和功能轨迹 不包含周期、缓存、频率、能耗

体系结构四层怎样分工

ISA 层回答“这条指令承诺什么”。add 把两个源寄存器的低 XLEN 位结果写到目标寄存器;lw 从地址读一个字;bne 在两个寄存器不等时改变 pc。RISC-V 规范还规定 x0 总是 0,所以执行器每步都把 x0 强制保持为 0。

微架构层回答“怎样实现这些承诺”。一台简单处理器可以顺序取指、译码、执行、访存、写回;一台高性能处理器可能预测分支、乱序执行和重命名寄存器。只要提交到 ISA 可见状态时与顺序语义一致,它们都可以运行同一段程序。第 09–14 篇会逐步解释为什么内部乱序仍要按架构状态收束。

实现工艺层回答“这些电路能跑多快、耗多少电”。同样是 add,不同工艺、频率、电压和布线都会改变延迟与能耗。体系结构课通常不从晶体管物理开始,但必须承认真实机器不是抽象公式。

系统软件层回答“程序怎样获得运行环境”。真实 C 程序还有 ABI、栈、页表、异常、调度和设备。第 04 篇讲函数调用边界,第 20–21 篇讲地址翻译和异常。本篇故意用裸输入避开操作系统,目的是先把 ISA 可见状态固定住。

入口自测

以下五道题用于判断是否需要先补基础。每道题的答案都能从本篇案例推出。

  1. 0xfff10113 这条 addi x2,x2,-1 执行前 x2=5,执行后是多少?
  2. 5 个 32 位元素一共占多少字节?如果起始地址是 128,最后一个元素地址是多少?
  3. x0 如果被写入 123,下一步还能读到 123 吗?
  4. 第 27 步 bne x2,x0 没有跳转,说明哪个寄存器变成了什么值?
  5. 1 ns、1 µs、1 ms 各相差多少倍?mem148=25 能不能证明真实 CPU 的缓存命中?

参考答案:1. 4。2. 5 × 4 = 20 字节,地址是 144。3. 不能,x0 在 RV32I 程序员模型中硬连为 0,本执行器也保持这个约束。4. x2=0,与 x0 相等,所以不跳转。5. 1 ms = 1000 µs = 1,000,000 ns;不能证明缓存命中,因为这是功能执行输出,只记录架构内存结果,不记录缓存访问。

验收结果

本批实际运行:

1
bash examples/computer-architecture/base/scripts/run_all.sh

结果为 base regression: PASS。同名素材目录保存 base-environment.txtsum.hex.txtsum-trace.jsonl.txt。本机环境记录为 macOS arm64、Apple clang 21.0.0、Python 3.14.4。

本篇验收成立的部分:数组求和从 C 源码、本机 AArch64 汇编、手写 RV32I 机器码、寄存器轨迹、分支和最终内存写回形成一条可检查路径。保留缺口:没有真实硬件计数器;没有流水线、缓存、页表或分支预测证据。

练习

练习 1:如果把数据改成 {1, 1, 2, 3, 5},在不改变指令的情况下,最终 mem148 应是多少?

答案:12。程序只依赖 5 个连续 32 位字,循环次数仍是 5。

练习 2:如果初始 x2 错设为 4,而数据仍是 {3, 4, 5, 6, 7},最终写回什么?这属于 ISA 错误、程序错误还是微架构错误?

答案:18,只加到前四个元素。这是程序输入或机器码层面的语义错误;只要处理器忠实执行这段指令,就不是微架构错误。

参考资料