“假设在内存中加载和存储值需要 100 个周期加上指令本身的成本。”
这没什么意义。除非您应该假设指令获取也很慢,否则这就像有一个指令缓存但没有数据缓存。
(或者运行程序时将数据内存映射到不可缓存的内存区域。)
正如@Martin 指出的那样,您可以组成任何您想要的数字并进行相应的模拟,即使没有合理的工程原因需要以这种方式构建 CPU。
如果您尝试在主机 CPU 上模拟 MARS 等模拟器本身的性能,那么加载/存储的成本也不是特别合理。每条指令的解释器成本因分支预测在主机 CPU 上的工作情况而有很大差异,而模拟客户内存只是解释器模拟器工作的一小部分。
现代 x86 上的负载对于 L1d 缓存命中(从准备好地址到准备好数据)通常具有 5 个周期延迟,但它们也具有每时钟 2 个吞吐量。因此,即使没有任何缓存未命中,也可以在英特尔 Sandybridge 系列 CPU 或 AMD K8 / Bulldozer / Zen 的两个流水线负载执行单元中同时运行多达 10 个负载。 (使用负载缓冲区来跟踪缓存未命中负载,可以在整个乱序后端同时运行更多负载。)
除非您谈论的是周围代码的特定上下文,否则您不能说负载在这样的 CPU 上“花费”了 5 个周期,例如遍历链表,因为下一次加载的地址取决于当前加载的结果。
在遍历数组时,您通常会使用 add 指令(或 MIPS addui)生成下一个指针,该指令有 1 个周期延迟。即使负载在简单的有序流水线上有 5 个周期的延迟,展开 + 软件流水线也可以让您在每个时钟周期维持 1 个负载。
在流水线 CPU 上,性能不是一维的,您不能只将成本数字放在指令上并将它们相加。即使对于带有乘法指令的经典有序 MIPS 上的 ALU 指令,您也可以看到这一点:如果您不立即使用 mflo / mhi,则乘法延迟会被您填补该空白的任何指令隐藏。
正如@duskwuff 指出的那样,像第一代 MIPS 一样的classic RISC 5-stage pipeline (fetch/decode/exec/mem/write-back) 假设缓存命中具有 1 个时钟的内存吞吐量和访问 L1d 本身的延迟。但是 MEM 阶段为加载(包括 EX 阶段中的地址生成)留出了 2 个周期延迟的空间。
而且我猜他们也不需要存储缓冲区。更复杂的 CPU 使用存储缓冲区将执行与可能在 L1d 缓存中丢失的存储分离,即使缓存未命中也隐藏存储延迟。这很好,但不是必需的。
早期的 CPU 通常使用简单的直接映射虚拟寻址缓存,从而在不降低最大时钟速度的情况下实现如此低的缓存延迟。但是在缓存未命中时,管道会停止。 https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Cache_miss_handling.
更复杂的有序 CPU 可以记分板加载,而不是在它们中的任何一个未在缓存中丢失时停止,并且只有在后面的指令尝试实际读取由尚未完成的加载最后写入的寄存器时才会停止。这允许 hit-under-miss 和多个未完成的缓存未命中来创建内存并行性,从而允许同时进行多个 100 周期的内存访问。
但幸运的是,您的循环首先不包括任何内存访问。它是纯 ALU + 分支。
在带有分支延迟槽的真实 MIPS 上,你可以这样写:
li $t0, 10 # loop counter
li $a0, 10 # total = counter # peel the first iteration
Loop: # do{
addi $t0, $t0, -1
bgtz $t0, Loop # } while($t0 > 0);
add $a0, $a0, $t0 # branch-delay: always executed for taken or not
这仍然只是 10+9+8+...+0 = (10*11)/2 最好用乘法而不是循环来完成。但这不是重点,我们正在分析循环。我们执行相同数量的添加,但我们在末尾添加 += 0 而不是在开头添加 0 + 10。
注意我使用了the real MIPS bgtz instruction,而不是$zero 的bgt 伪指令。希望汇编程序会为$zero 的特殊情况选择它,但它可能只是遵循使用slt $at, $zero, $t0 / bne $at, $zero, target 的正常模式。
经典 MIPS 不做分支预测 + 推测执行(它有一个分支延迟槽来隐藏控制依赖的气泡)。但要让它工作,it needs the branch input ready in the ID stage,所以读取前一个add 的结果(在 EX 结束时产生结果)将导致 1 个周期停顿。 (或者更糟,取决于是否支持转发到 ID 阶段。https://courses.engr.illinois.edu/cs232/sp2009/exams/final/2002-spring-final-sol.pdf 问题 2 部分(a)有一个这样的例子,但我认为如果你需要等待 add WB 之前完成,他们会低估停顿周期bne/bgtz ID 可以启动。)
因此,无论如何,这应该在标量有序 MIPS I 上每 4 个周期运行 1 次迭代,可以从 EX 转发到 ID。 3 条指令 + 每个 bgtz 之前的 1 个停顿周期。
我们可以通过将add $a0, $a0, $t0 放在循环计数器和分支之间来优化它,用有用的工作填充这个停顿循环。
li $a0, 10 # total = counter # peel the first iteration
li $t0, 10-1 # loop counter, with first -- peeled
Loop: # do{
# counter-- (in the branch delay slot and peeled before first iteration)
add $a0, $a0, $t0 # total += counter
bgtz $t0, Loop # } while($t0 > 0);
addi $t0, $t0, -1
这以 3 个周期/迭代运行,3 条指令没有停顿周期(再次假设从 EX 转发到 ID)。
将counter-- 放在分支延迟槽中使其尽可能早于下一个 执行循环分支。一个简单的bne 而不是bgtz 也可以;我们知道循环计数器从有符号正数开始,每次迭代减 1,因此我们不断检查非负数和非零数并不重要。
我不知道您使用的是什么性能模型。如果它不是经典的 5 阶段 MIPS,那么以上内容无关紧要。