抽象层次与接口契约¶
动机:复杂性必须被切开¶
数十亿晶体管不可能由应用程序员逐个控制。工程上的办法不是消灭复杂性,而是建立边界:上层只依赖下层承诺的行为,下层可以在不破坏承诺的前提下更换实现。这就是抽象。
常见层次可以写成:
箭头不是单向“翻译”。异常从硬件向操作系统报告,性能计数器向软件暴露微体系结构现象,编译器也会依据流水线和 cache 特性安排代码。
接口与实现¶
接口说明使用者可依赖什么,实现说明承诺怎样兑现。以加法指令为例,ISA 会规定操作数来自哪些寄存器、结果截断到多少位、程序计数器如何变化;它通常不规定这次加法由一拍完成、分成多级流水,还是与其他指令乱序重叠。
这带来两个好处:
- 软件生态可跨处理器代际复用;
- 硬件设计者可以用面积、功耗和性能之间的不同权衡实现同一语义。
代价是接口一旦广泛部署就很难修改。历史兼容性会让旧指令、旧异常行为和旧地址模式长期存在。
最小完整例子:z = x + 1¶
假设 x 和 z 位于内存,编译器生成简化指令:
各层分别做什么?
| 层次 | 对这个例子的职责 |
|---|---|
| 语言 | 规定整数加法和溢出的语言语义 |
| 编译器 | 选择 lw/addi/sw,分配寄存器 |
| ISA | 规定三条指令的编码与可见效果 |
| 微体系结构 | 取指、译码、计算地址、访问 cache、写回 |
| 数字逻辑 | 用加法器、多路选择器、寄存器和状态机实现数据流 |
| 器件 | 在电压、时序和功耗约束下实现逻辑 0/1 |
这个例子“麻雀虽小,五脏俱全”:有指令、有状态、有计算、有访存,也有潜在的流水线相关和 cache miss。第 8 章会把它展开成逐周期路径。
架构状态与瞬时状态¶
程序正确性关注架构状态:通用寄存器、程序计数器、内存和体系结构定义的控制状态。处理器内部还有预测表、流水寄存器、重排序缓冲区等微体系结构状态。发生分支预测错误时,瞬时状态可以被清空,但已经提交的架构状态不能被错误路径污染。
这一区分与数据库事务很像:内部可以投机执行,但“提交”前不能让外界观察到不一致结果。
抽象会泄漏¶
抽象保证功能语义,不必保证恒定性能。数组按行访问和按列访问在语言层等价,却可能因 cache 局部性产生巨大差别;浮点加法满足 IEEE 754 语义,却不满足实数加法的结合律。安全领域中的侧信道更直接利用了时间、cache 或功耗这些原本未写进功能接口的信号。
因此更准确的直觉是:抽象建立可依赖的最低契约,而不是让下层永远不可见。
与其他课程的联系¶
- 数据结构讨论访问模式,组成原理解释访问模式为何影响 cache。
- 编译原理把高级程序映射到 ISA,并做机器相关优化。
- 操作系统管理异常、中断、页表和设备,但依赖硬件提供机制。
- 数字电路提供组成部件,组成原理解释部件怎样构成可编程机器。
自测¶
- “x86 是一种微体系结构”这句话哪里不准确?
- 分支预测器出错为何通常只影响性能,不改变正确结果?
- 列举一个功能抽象没有泄漏、但性能抽象发生泄漏的例子。