我到处搜索,我收集了管道或其他东西。我已经检查了其他程序,看起来像是一个单周期和多周期:Clock cycles of Single-cycle and multi-cycle MIPS
我如何区分什么周期。
例如,这将是多少个时钟周期:
li $a0, 0 # $a0 = 0
li $t0, 10 # Initialize loop counter to 10
Loop:
add $a0, $a0, $t0
addi $t0, $t0, -1 # Decrement loop counter
bgt $t0, $zero, Loop # If ($t0 > 0) Branch to loop
我的教授给了我一个任务:“假设在内存中加载和存储值花费100个周期加上指令本身的成本。”
根据我的阅读,加载是5个周期,所以我的教授为什么要说100个周期。我能把它变成我想要的任何数字吗?我很困惑。
这个问题没有意义。
大多数教育MIPS实现中使用的标准多周期RISC流水线基本上是基于可以在一个周期内同时访问程序和数据存储器的要求。 “假设在内存中加载和存储值花费100个周期”将需要完全不同的架构。
我们必须区分两种情况:
案例1:MIPS模拟器
根据我的阅读,加载是5个周期,所以我的教授为什么要说100个周期。
您不使用真正的CPU,而只使用模拟CPU。因此,争论你的程序在模拟CPU上“真正”需要多少循环是没有意义的。
也许模拟器为每个内存访问模拟5个周期。但是,另一个模拟器可以模拟10个周期或仅1个周期用于存储器访问。
这意味着在谈论模拟循环的数量时,您将不得不说出使用了哪个模拟器。你的教授说要假设一个模拟100个周期的模拟器。
案例2:真正的MIPS CPU
我能把它变成我想要的任何数字吗?
在这种情况下,您必须检查CPU的手册以获得CPU所需的实际周期数。
但是,实际MIPS类型CPU的指令集与“MIPS”仿真器的指令集不是100%相同。在你的程序中,bgt
指令的工作方式不同。
这意味着我们也无法争论您的程序在真正的MIPS CPU上需要多少个周期,因为我们必须先修改它才能在真正的MIPS CPU上运行它 - 这可能会改变所需的周期数。
如果您想知道在使用真实CPU时数字100是否合理:
正如我们从“幽灵”和“崩溃”安全漏洞中所知,在真实CPU上读取内存所需的时间在很大程度上取决于CPU缓存的状态。如果我们假设a0
指向内存映射外设(从不缓存),则100个周期可能是合理的。
“假设在内存中加载和存储值需要100个周期加上指令本身的成本。”
这没什么意义。除非您假设指令获取速度慢,否则这就像拥有指令缓存但没有数据缓存。
(或者运行程序,数据存储器映射到不可缓存的内存区域。)
正如@Martin指出的那样,你可以编制你想要的任何数字并进行相应的模拟,即使没有合理的工程原因可以用这种方式构建CPU。
如果你试图在主机CPU上模拟像MARS这样的仿真器的性能,那么加载/存储也不会特别合理。每条指令的解释器成本因分支预测在主机CPU上的工作情况而异,并且仿真客户机内存只是解释仿真器的一小部分。
现代x86上的负载通常具有5个周期延迟,用于L1d高速缓存命中(从准备好的地址到准备就绪的数据),但它们也具有每时钟2个吞吐量。因此,即使没有任何高速缓存未命中,也可以在Intel 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阶段为负载留出了2个周期的延迟空间(包括EX阶段的地址生成)。
我猜他们也不需要商店缓冲区。更复杂的CPU使用存储缓冲区将执行与可能在L1d缓存中丢失的存储分离,即使对于缓存未命中也会隐藏存储延迟。这很好,但不是必需的。
早期的CPU通常使用简单的直接映射虚拟寻址高速缓存,从而实现如此低的高速缓存延迟,而不会削弱最大时钟速度。但是在缓存未命中时,管道会停止运行。 https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Cache_miss_handling。
更复杂的有序CPU可以记分加载而不是在其中任何一个在高速缓存中丢失时停止,并且仅在稍后的指令试图实际读取最后由尚未完成的加载写入的寄存器时停止。这允许命中未命中和多个未完成的高速缓存未命中以创建存储器并行性,允许多个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,而不是使用bgt
的$zero
伪指令。希望汇编程序会选择$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)有一个这样的例子,但我认为如果你需要等待在bne /之前完成添加WB,它们会计算失速周期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,那么上述内容就无关紧要了。