一句话结论PC程序计数器不会等 SIMD 指令拿到数据后再主动 1。在现代处理器里PC 默认一直向前走数据没到时硬件只会拦住真正需要这份数据的指令。引子一个很自然的猜想学习体系结构时很多人会这样理解 SIMD 访存一条 SIMD Load 指令在一个时钟周期内读不到数据于是 CPU 等几个周期数据到了以后这条指令再主动把 PC 1执行下一条。这个模型很直观也基本符合最早期计算机的工作方式但它包含三个不太准确的假设隐含假设实际情况① 下一条指令地址是PC 1通常是PC 指令长度② 指令会主动修改 PCPC 由处理器的控制逻辑更新③ 必须等数据回来PC 才能前进现代处理器中PC 前进和数据返回基本是解耦的下面逐一说明再看不同类型的处理器分别怎么处理数据还没到这件事。一、先把两个基础概念摆正1. PC 加的是指令长度不是 1主流架构的内存按字节寻址PC 保存的是字节地址架构下一条顺序指令地址RISC-VPC 4压缩指令为PC 2ARM64PC 4x86 / x86-64PC 当前指令长度115 字节部分 DSP、教学 CPU按字寻址时可能是PC 1所以PC 1只在特定的按字寻址设计中成立。2. 指令不会主动做任何事指令只是一串编码。读取指令、译码、更新 PC 的是硬件控制逻辑例如多周期 CPU 的状态机或流水线 CPU 的取指单元与冒险检测单元。因此更准确的问题应该是当一条 SIMD Load 需要多个周期才能拿到数据时处理器的控制逻辑如何处理 PC 和后续指令二、SIMD Load 为什么会需要多个周期一个周期读不到数据可能有几种不同的原因它们的处理方式也不同原因说明缓存延迟即使 L1 命中通常也要 45 个周期L2、L3 更久访问内存可能要几百个周期缓存未命中数据不在缓存中需要从下一级存储取回向量宽度超过数据通路例如 512 位向量而 Load 端口一次只能读 256 位需要分两拍跨缓存行访问一次向量访问跨越两个 64 字节缓存行需要拆成两次访问Gather 离散访问每个元素地址不同可能需要多次访问缓存前两种是等待外部数据后三种是指令本身需要多拍完成。下面会看到处理器对这两类情况都有对应机制。三、模型 1非流水线多周期 CPU这是最接近开头那个猜想的模型。多周期 CPU 用状态机控制每条指令FETCH → DECODE → EXECUTE → MEM → WRITEBACK → FETCH → ... │ mem_ready 0 时停在这里不过即使在这种 CPU 中PC 4也常常不是在数据返回后才计算。以经典教材中的多周期 MIPS 为例取指阶段就已经执行PC ← PC 4。always (posedge clk) begin case (state) FETCH: begin ir imem[pc]; pc pc 4; // 取指时就更新 PC state DECODE; end MEM: begin if (mem_ready) // 数据没到就停留在 MEM 状态 state WRITEBACK; end WRITEBACK: begin regfile[rd] mem_data; state FETCH; // 写回后才取下一条指令 end endcase end真正被延迟的不是PC 4而是下一次取指。PC 可能早已指向下一条指令只是状态机还没有回到FETCH。这种设计的问题很明显一次缓存未命中可能让整个处理器空等几百个周期。四、模型 2顺序流水线 CPU流水线 CPU 不会等一条指令完全结束再取下一条。正常情况下取指单元每个周期都会把 PC 往前推。周期: 1 2 3 4 5 6 7 vle v1,(a0) IF ID EX MEM WB vadd v2,v1,v3 IF ID ■ EX MEM WB ← 需要 v1插入一个气泡 vmul v4,v5,v6 IF ■ ID EX MEM ← 被前面的停顿一起拖住当后续指令依赖尚未返回的数据时冒险检测单元会冻结 PCPC 暂时不更新冻结 IF/ID 流水线寄存器已取出的指令保持原位向执行阶段插入气泡相当于插入 NOP数据返回后通过旁路转发解除停顿流水线继续前进。// Load-Use 冒险ID 阶段的指令要读取 EX 阶段 Load 的目标寄存器 wire load_use_hazard ex_is_load ((id_rs1 ex_rd) || (id_rs2 ex_rd)); // 缓存未命中时简单的阻塞式流水线会整体停顿 wire stall load_use_hazard || dcache_miss; always (posedge clk) begin if (!stall) pc next_pc; // 默认每周期前进停顿时保持不变 end准确的描述是PC 本来每个周期都会前进遇到数据冒险时被暂时拉住数据就绪后再放开。如果流水线使用非阻塞缓存和记分牌Load 未命中时与它无关的指令还能继续执行只有依赖这份数据的指令会停下来。五、模型 3向量单元内部的分拍执行如果 SIMD 指令需要多个周期是因为向量宽度超过了硬件数据通路那么处理器通常会在执行单元内部分拍处理而不是阻止 PC 前进。方式一拆成多个微操作例如 AMD Zen 1 的 256 位 AVX 指令会在内部拆成两个 128 位微操作执行vaddps ymm0, ymm1, ymm2 (256 位) ↓ 译码拆分 uop 1: 处理低 128 位 uop 2: 处理高 128 位方式二向量单元内部的序列器在很多 RISC-V 向量扩展实现中标量核心只负责把向量指令发送到向量单元。向量单元内部再按元素组逐拍执行标量核心: 取指 vle → 发给向量单元 → 继续取后续标量指令 │ 向量单元: 第 1 拍元素 07 第 2 拍元素 815 第 3 拍元素 1623 ...这类设计中标量 PC 可以继续前进只有当后续指令需要读取尚未写完的向量寄存器或者向量指令队列满了时才需要停下来。高性能向量处理器还常常支持链接Chaining前一条向量指令写出第一批元素后后一条依赖它的指令就可以开始处理这些元素不必等整条向量全部完成。六、模型 4乱序执行 CPU现代 x86 和高端 ARM 核心中PC 与数据返回几乎完全解耦。前端 后端 PC → PC4 → PC8 → ... ──▶ 寄存器重命名 借助分支预测持续取指 │ ▼ 保留站 / 调度队列 │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ vle v1,(a0) vadd v4,v5,v6 vmul v2,v1,v3 等待缓存返回 操作数就绪先执行 等待 v1 │ ▲ └──────── 数据返回广播唤醒依赖指令 ──────┘ │ ▼ 重排序缓冲区ROB按程序顺序提交当 SIMD Load 的数据返回时硬件会把数据写入物理寄存器广播这个寄存器已就绪唤醒等待它的指令等这条 Load 到达 ROB 头部后按程序顺序提交。这里有两个容易混淆的PC概念含义前端取指 PC分支预测器驱动的取指地址可能远远领先当前执行位置架构 PC / 提交位置软件可见的程序执行进度随 ROB 提交前进如果一定要找数据到了以后程序才越过这条指令的对应物那就是ROB 提交。Load 未完成时它会卡在 ROB 头部后面即使已经执行完的指令也不能提前提交。七、模型 5GPU 的 SIMT 架构GPU 处理访存延迟的方法更直接这个线程组等着就换另一个线程组执行。周期 1Warp 0 发出 Load数据需要较长时间返回 周期 2调度器选择 Warp 1 周期 3调度器选择 Warp 2 周期 4调度器选择 Warp 3 ... 周期 NWarp 0 的数据返回记分牌清除Warp 0 重新可以调度每个 Warp 有自己的执行位置Warp 0 可以越过 Load 指令但后续依赖该数据的指令会被记分牌阻止GPU 依靠大量 Warp 轮换来隐藏访存延迟。所以 GPU 也不是让 Load 指令拿到数据后推动 PC而是在数据未就绪期间安排其他工作。八、真正让 PC 停在原地的情况异常有一种情况下架构 PC 确实不能越过当前的 SIMD Load访存失败。例如向量 Load 访问到了未映射的页面处理器触发精确异常异常 PC 指向出错的这条 Load 指令后续指令的结果全部丢弃操作系统处理缺页返回后重新执行这条 Load。部分 SIMD/向量架构还支持可恢复的部分完成机制作用AVX-512 Gather完成的元素会清除对应掩码位重新执行时只处理剩余元素ARM SVE First-Fault Load用 FFR 寄存器记录哪些元素成功加载RISC-V 向量扩展可通过vstart记录从哪个元素继续执行这说明异常时 PC 停在原地不是因为等待数据而是因为这条指令没有成功完成。九、一个类比餐厅的点餐与出餐餐厅处理器前台持续接单取指单元持续推进 PC厨房做菜需要时间Load 等待缓存或内存返回数据不需要这道菜的顾客继续用餐无依赖的指令继续执行等这道菜的顾客只能等待依赖指令在流水线或保留站中等待菜做好后叫号数据返回唤醒依赖指令按订单顺序结账ROB 按程序顺序提交前台不会因为一道菜还没做好就停止接单厨房出菜后也不会去替前台写下一张订单。十、总结对比处理器类型PC 如何前进SIMD Load 数据未到时非流水线多周期 CPU常在取指阶段计算PC 4状态机停在访存状态暂不取下一条顺序流水线 CPU默认每周期前进遇到冒险时冻结 PC、插入气泡分拍执行的向量单元标量前端可继续取指向量单元内部逐拍执行相关指令等待乱序执行 CPU前端预测性前进架构 PC 随提交前进依赖指令等待唤醒Load 卡在 ROB 头部时阻止提交GPU SIMT每个 Warp 独立推进切换到其他 Warp记分牌阻止依赖指令访存异常架构 PC 停在出错指令处处理异常后重新执行该指令结语开头的猜想把三件事绑在了一起指令完成、数据返回、PC 前进。在现代处理器中它们是分开处理的PC 的前进由取指和分支预测逻辑负责数据的返回由缓存、Load 队列和向量单元负责依赖关系由互锁、记分牌、保留站负责程序顺序由 ROB 提交和精确异常保证。所以比数据到了再把 PC 1更准确的说法是PC 默认持续向前数据没到时硬件要么暂停流水线要么让依赖指令等待要么切换到别的线程。只有指令真正完成并提交后程序的架构执行位置才会越过它。延伸阅读《Computer Organization and Design: RISC-V Edition》多周期 CPU、流水线冒险与旁路《Computer Architecture: A Quantitative Approach》乱序执行、记分牌、Tomasulo 算法、向量处理器RISC-V Vector Extension Specificationvl、vstart与向量异常恢复Arm SVE Architecture ReferenceFirst-Fault Load 与 FFRIntel Software Developer’s ManualAVX-512 Gather 的掩码行为与异常处理
阅读完成 · 觉得有帮助?