# 引言
# 计算机体系结构的核心问题
体系结构要回答的问题包括:
- 指令集设计:程序员看到的 “机器语言接口” 该是什么样?(RISC 还是 CISC?寄存器多少?寻址方式?)
- CPU 设计思想:如何利用流水线、并行、乱序执行等手段提升指令执行效率?
- 存储系统组织:如何设计多级 Cache、内存一致性、带宽 / 延迟优化?
- I/O 与互连:CPU 如何高效与外设 / 网络通信?
- 性能与功耗权衡:如何在速度、成本、能耗之间平衡?
- 可靠性与安全性:容错、冗余、安全隔离等机制。
# 缓存
# 为什么要缓存:
局部性原理 (Principle of Locality),即程序倾向于重用最近访问过的数据和指令(时间局部性)以及访问地址相邻的数据(空间局部性)
# 内存墙 (The Memory Wall)
处理器性能与主存访问延迟之间的性能差距正日益扩大
- 应对:设计者采用了多级缓存、独立的指令和数据缓存、多端口和流水线化缓存等多种技术。并行技术(如乱序执行、多线程)在很大程度上也是为了隐藏内存延迟
# 基本工作原理
-
缓存命中(Cache Hit):如果在缓存中找到数据,处理器可以快速获取
-
缓存失效(Cache Miss)
:如果数据不在缓存中,则称为 “缓存失效”, 下面是 3C 模型
- 冲突失效(Conflict Miss): 多个内存块(memory blocks)映射到缓存中的同一个位置 (有重叠),重叠的会被替换,再访问的时候 miss 的情况
- 强制失效 (Compulsory Miss):当第一次访问一个内存块时,该块肯定不在缓存中,必须从主存中调入。
- 容量失效 (Capacity Miss):当程序需要访问的数据块总量超过了缓存的总容量时,即使缓存是全相联的,也必须替换掉一些块以便为新块腾出空间。
-
块(Block)
cache miss 时候,硬件必须从下一级存储器(如主存)中获取包含该数据的 block, 并将其放入缓存
- 由于空间局部性(程序倾向于访问地址相邻的数据),系统一次会移动一整个数据块(或称为 “行”,Line)到缓存中
-
缓存性能的核心量化指标
- 失效率(Miss Rate):这是最基本的性能指标之一,指缓存失效次数占总访问次数的比例
- 命中时间(Hit Time):指从缓存中成功获取数据所需的时间
- 失效开销(Miss Penalty):指因缓存失效而额外花费的时间,即从下一级存储器获取数据块并将其放入缓存所需的时间
# 缓存设计中的四个基本问题
# 块放置策略(Block Placement)
决定一个内存块可以被放置在缓存的哪个位置
- 直接映射(Direct Mapped):每个内存块只能映射到缓存中的一个固定位置。这是最简单的策略,但容易产生冲突失效。
- 全相联 (Fully Associative):这种策略与直接映射相反,一个内存块可以被放置在缓存中的任何位置。由于其极大的灵活性,全相联缓存不会有冲突失效,只会产生强制失效和容量失效。它的硬件实现非常复杂和昂贵,因此很少用于主缓存。
- 组相联 (Set Associative):这是直接映射和全相联之间的一种折中,也是现代处理器缓存最常用的策略。它将缓存块分成若干个 “组”(Set),一个内存块被映射到一个特定的组,但可以在该组内的任何块位置自由放置。https://i.ytimg.com/vi/KhAh6thw_TI/maxresdefault.jpg
# 块识别策略(Block Identification)
如何在缓存中找到所需的块
- 通过标签(Tag) 来实现。每个缓存块都附有一个地址标签,用于存储该块对应的内存地址的高位部分
- 索引 (Index):地址的中间部分。
- 块内偏移 (Block Offset):地址的最低位部分
- 有效位(Valid Bit) 用于指示一个缓存块中的数据是否有效
- 寻找流程:
- 使用 “索引 (Index)” 定位到唯一的 “组 (Set)
- 在 “组” 内使用 “标签 (Tag)” 进行并行比较
- 使用 “块内偏移 (Block Offset)” 在块内找到具体数据
# 块替换策略(Block Replacement)
当发生缓存失效且没有空闲块时,选择哪个块被替换出去
- 最近最少使用(LRU):替换最长时间未被访问的块,旨在更好地利用时间局部性
- 随机(Random):随机选择一个块进行替换,硬件实现简单
- 先进先出(FIFO):替换最早进入缓存的块
# 写策略(Write Strategy)
当发生写操作时如何处理
- 写直通(Write-Through):同时更新缓存和下一级存储器。实现简单,但会增加内存流量。
- 写回(Write-Back):只更新缓存中的数据,并使用一个 “脏位”(Dirty Bit)标记该块已被修改。只有当这个 “脏” 块被替换时,才会将其写回下一级存储器
# 缓存优化技术
- 减少命中时间 (Hit time): 采用小而简单的一级缓存 (Small and simple first-level caches) 是关键策略,在缓存索引期间避免地址转换
- 降低失效率 (Miss rate): 增加块大小,提高相联度, 硬件预取:硬件预测未来可能需要的指令或数据,并提前将其从较低层内存加载到缓存
- 减小失效开销 (Miss penalty): 多级缓存, 优先处理读缺失而非写操作, 关键词优先和提前重启,非阻塞缓存 / 缺失下命中
# 保护机制的需求与虚拟内存
随着多道程序(multiprogramming)的出现,多程序并发运行,需要新的机制来保护程序间的独立性和共享资源
# 虚拟内存
-
虚拟内存通过地址转换提供保护
-
处理器生成的虚拟地址通过硬件和软件结合的方式转换为物理地址,以访问主内存。这个过程称为内存映射或地址转换
# 虚拟内存系统中的主要组件
-
页(Pages):虚拟地址空间被划分为固定大小的块,称为页。这些页在主内存或磁盘之间移动。段(Segmentation)
- 当处理器引用一个位于不在缓存或主内存中的页中的数据项时,会发生页错误(page fault),此时整个页将从磁盘移到主内存中
-
段(Segments)
- 思想:把程序按照逻辑结构划分为若干段(Segment),每个段是一块连续的内存区域。
- 常见的段:代码段(存放指令)、数据段(存放全局 / 静态变量)、堆段(动态分配)、栈段(函数调用的局部变量)。
-
页表(Page Tables):页表存储在主内存中,用于将虚拟页号映射到物理页帧地址。它们还包含保护字段、有效位、使用位和脏位等信息。操作系统通过修改页表中的这些位来改变页面权限。
-
转译后备缓冲器(Translation Lookaside Buffer, TLB):
-
虚拟地址 → 物理地址 需要经过 页表 映射。页表通常很大,存放在主存中。
-
一次访存 = 查页表(一次内存访问)+ 真正取数据(一次内存访问) → 总共要 两次内存访问。
-
利用局部性原理(程序往往集中访问少量页面),把最近用过的虚拟页到物理页的映射缓存起来。
TLB 就是这种高速缓存,位于 CPU 芯片内部,访问速度接近寄存器。
-
# 虚拟机器(Virtual Machines)
虚拟机器被定义为 “真实机器的高效、隔离的副本”。** 虚拟机器监视器(Virtual Machine Monitor, VMM)** 能够完全控制系统资源,并为程序提供与原始机器基本相同的环境
虚机的核心目的在于:
-
隔离与安全:在现代系统中日益重要的隔离和安全需求。
-
标准操作系统的安全与可靠性缺陷:作为对传统操作系统安全和可靠性不足的一种应对方案。VMM 的代码库远小于传统操作系统(例如,隔离部分可能只有 10,000 行代码,而 OS 有数百万行),这减少了潜在的漏洞
-
共享与整合:允许在数据中心或云环境中,许多不相关的用户共享一台计算机。它允许多个独立的软件栈共享硬件,从而整合服务器数量
# 虚机对底层架构的要求与挑战
-
ISA 支持:理想情况下,指令集架构(ISA)在设计时就应考虑到虚拟化,以减少 VMM 需要模拟的指令数量及其模拟时间。
-
问题指令:对于 80x86 架构,有 “18 条指令会给虚拟化带来问题
-
虚拟内存虚拟化:每个客户机 OS 管理自己的页表,这使得虚拟内存的虚拟化变得复杂。VMM 引入了 “真实内存” 作为虚拟内存和物理内存之间的一个中间
- 影子页表(Shadow Page Tables):为避免每次内存访问额外的间接寻址开销,VMM 会维护一个影子页表,直接将客户机虚拟地址空间映射到硬件的物理地址空间。VMM 必须捕获客户机 OS 对页表或页表指针的任何修改尝试。
- TLB 虚拟化:RISC 架构通常通过 TLB 的进程 ID 标签支持不同 VM 和 VMM 的条目共存,避免在 VM 切换时刷新 TLB。而 80x86 的 TLB 不支持进程 ID 标签,导致共享 TLB 成本更高,每次地址空间改变通常需要刷新 TLB。
-
I/O 虚拟化:“系统虚拟化中最困难的部分” 是 I/O 虚拟化,原因在于 I/O 设备数量和类型日益增多,以及多个 VM 共享真实设备的复杂性。
# 指令级并行 (ILP) 的概念与挑战
ILP 的思想就是,我们能不能让多个步骤同时进行? ILP 就是让 CPU 在同一时间点,同时处理多个指令的不同阶段,以此来榨干 CPU 的性能。
# CPI
流水线 CPI (每条指令的时钟周期数) 来衡量。
# 基本编译器技术的重要性
- 核心目标:让硬件(尤其是流水线处理器和 VLIW 处理器)更高效地执行指令。
- 为什么重要:
- 现代 CPU 的执行速度很快,但指令之间往往存在 数据依赖、控制依赖 等限制,导致流水线停顿。
- 编译器可以通过 重排、展开、调度 来 “制造” 更多独立的指令,让处理器保持高利用率。
# 关键目标
- 暴露 ILP(指令级并行):
通过代码变换(如循环展开),让指令间依赖更少,能并行执行的指令更多。 - 降低流水线停顿:
减少数据冒险、控制冒险带来的空周期。 - 支持静态调度处理器:
对 VLIW 或依赖编译器调度的处理器而言,编译器必须主动生成并行度高的代码,否则硬件性能发挥不出来。
# 代表性技术:循环展开(Loop Unrolling)
- 原理:复制循环体,让多次迭代并列展开 → 产生更多可调度的独立指令。
- 好处:
- 消除循环控制开销(减少分支)。
- 增加并行度,让流水线和功能单元得到更充分利用。
- 挑战:
- 需要额外寄存器,否则会产生资源冲突。
- 代码尺寸膨胀,可能导致指令缓存压力。
- 应用场景:
- 在 VLIW 处理器中特别重要,因为它们依赖编译器产生 “长直线” 代码序列来填满一个周期内的多个执行槽位。
# 冒险 (Hazards)
依赖性 (Dependences) 是程序的固有属性 冒险 (Hazards) 则是流水线组织是否能检测到这些依赖性并因此导致停顿的属性
# 数据冒险 (Data Hazards)
上一个步骤没做完,解释:
你不能先把生肉饼放到面包里,再拿去烤。必须先烤好肉饼 (写入结果),才能把它夹进面包 (读取结果)。这个先后顺序不能乱。
我们把这个依赖关系掰开来看,就成了三种情况:
** 写后读 (RAW - Read After Write) **
- 指令:指令 i: 算出 a = 2 + 3 -> 指令 j: 算出 b = a + 4
- 厨房例子:“先烤好肉饼 (i),才能组装汉堡 (j)。” 指令 j 必须等 指令 i 把 a 的值算出来写好后,才能读取 a 的值来用。如果搞反了,j 拿到的就是 a 的旧值,汉堡就做错了。这是最常见、最核心的数据依赖。
写后写 (WAW - Write After Write) -> “别把新结果给覆盖了”
- 指令:指令 i: a = ... -> 指令 j: a = ... (两条指令都要修改 a,但可能因为流水线乱序,i 后执行完)
- 厨房例子:经理 (i) 和一个新手 (j) 都在更新今天的 “特价菜单”(变量 a)。经理写的是 “今日特价:鸡腿堡”,新手写的是 “今日特价:牛肉堡”。按理说应该以新手 (后面那条指令) 的为准。但如果因为新手动作慢,经理后把他的牌子挂上去了,就把正确的结果给覆盖了。这就是 “写后写” 冒险。
读后写 (WAR - Write After Read) -> “等我用完你再改”
- 指令:指令 i: b = a + 1 -> 指令 j: a = 5
- 厨房例子:我 (i) 正在照着旧菜单 (a)** 给顾客点餐,你 (j) 别急着把旧菜单拿走,换上 ** 新菜单 (把 a 改成新值)。你必须等我 (i) 读完旧菜单的值下完单,你 (j) 才能去更新菜单。否则我点出来的餐就错了。
# 结构冒险 (Structural Hazards) -> “设备不够用”
- 学术说法:硬件资源不足,无法同时支持所有指令组合。
- 大白话解释:
厨房里只有一台炸薯条的机器。现在来了两份订单,都要炸薯条。A 厨师占了机器,B 厨师就只能干等着。这就是 “结构冒险”。因为硬件资源(炸锅)只有一个,导致流水线卡住了。- 解决方法:多买一台炸锅!(在 CPU 里就是增加更多的执行单元或端口)。
# 控制冒险 (Control Hazards) -> “顾客临时改主意”
-
学术说法:指令的执行顺序受分支指令的控制依赖影响。
-
大白话解释:
这就像程序里的 if-else 语句。- if (顾客要汉堡)
- 就去准备汉堡的材料;
- else
- 就去准备炸鸡的材料;
厨房的流水线为了快,可能会猜测:“嗯,十个顾客里有八个要汉堡,我猜这个也要汉堡!” 于是提前把汉堡的肉饼、面包都准备好了。
- 结果猜对了:太棒了!流水线一点没停顿,出餐飞快。
- 结果猜错了:顾客说 “我要炸鸡”。那之前为汉堡准备的所有东西(已经进入流水线的指令)都得全部扔掉,流水线清空,然后重新开始准备炸鸡的材料。这个 “清空重来” 的过程就是一次巨大的延误。这就是 “控制冒险”。
# 高级分支预测
流水线性能依赖连续供给指令:处理器通过流水线实现并行,如果遇到分支,下一条指令地址不确定,流水线可能会空转。
# 高级分支预测的目标
- 减少分支惩罚:尽量避免因预测错误带来的流水线停顿。
- 保持指令流顺畅:在分支结果未知时,仍能继续取指和发射,避免浪费周期。
- 提升预测准确率:通过复杂预测器(历史信息、多级预测、目标缓存等)尽可能降低误预测率。
- 支持推测执行:为乱序执行和推测执行提供稳定的预测,扩大可利用的 ILP。
# 关键高级分支预测技术
# 关联分支预测(Correlated Prediction)
两级预测器由两部分组成:
- 全局历史寄存器(GHR, Global History Register)
保存最近 N 条分支的走向序列(用移位寄存器,1 表示 taken,0 表示 not-taken)。 - 预测表(Pattern History Table, PHT)
用 GHR(有时结合 PC 的低位)作为索引,PHT 的每个条目通常是一个 2 位饱和计数器,用于给出 taken/not-taken 的预测并按分支结果更新。
# 分支目标缓冲器(BTB, Branch Target Buffer)
- 缓存分支目标地址,使处理器在指令解码前就知道跳转到哪。
- 如果命中,分支可能实现 “零延迟”;不命中则更新表项并付出惩罚。
# 返回地址栈(RAS, Return Address Stack)
- 专门预测函数调用的返回地址。
- 避免调用 / 返回过程中频繁误预测。