我到处搜寻,并收集了管道或其他东西。我检查了其他程序,似乎有一个单周期和多周期: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个周期。我可以随便问一个数字吗?我很困惑
答案 0 :(得分:3)
这个问题没有道理。
大多数教育性MIPS实现中使用的标准多周期RISC管道从根本上基于以下要求:程序和数据存储器都可以在单个周期中同时访问。要“假设在内存中加载和存储值要花费100个周期”,则需要一种完全不同的体系结构。
答案 1 :(得分:2)
我们必须区分两种情况:
案例1:MIPS模拟器
根据我的阅读,加载是5个周期,所以我的教授为什么要说100个周期。
您不是在使用真正的CPU,而只是在使用模拟的CPU。因此,争辩说程序在模拟CPU上“真正”需要多少个周期是没有意义的。
也许模拟器为每次内存访问模拟5个周期。但是,另一个模拟器可以模拟10个周期或仅1个周期来进行内存访问。
这意味着您在谈论模拟周期数时必须说出哪个模拟器。而且您的教授说,应该假设有一个模拟100个周期的模拟器。
案例2:真正的MIPS CPU
我可以给我想要的电话号码吗?
在这种情况下,您必须查看CPU的手册以获取CPU所需的实际周期数。
但是,实际的MIPS类型CPU的指令集与“ MIPS”仿真器之一并不完全相同。在您的程序中,指令bgt
的工作方式有所不同。
这意味着我们也无法争辩您的程序在真正的MIPS CPU上需要多少个周期,因为我们必须对其进行修改才能在真正的MIPS CPU上运行它-这可能会改变所需的周期数。 / p>
如果您想知道使用真实CPU时数字100是否合理:
从“ Spectre”和“ Meltdown”安全漏洞中我们知道,根据CPU缓存的状态,大量读取实际CPU上的内存所需的时间。如果我们假设a0
指向内存映射的外围设备(从不缓存),则可能有100个周期。
答案 2 :(得分:1)
“假设在内存中加载和存储值要花费100个周期,再加上指令本身的花费。”
这没有什么意义。除非您假定指令提取也是如此慢,否则这就像有指令缓存但没有数据缓存一样。
(或者在程序运行时将数据内存映射到不可缓存的内存区域。)
正如@Martin指出的那样,即使没有合理的工程理由以这种方式构建CPU,您也可以组成所需的任何数字并进行相应的模拟。
如果您要在主机CPU上对诸如MARS本身之类的仿真器的性能进行建模,那么加载/存储也是如此昂贵仍然不是特别合理。每条指令的解释器成本与分支预测在主机CPU上的运行状况有很大不同,模拟来宾存储器只是解释模拟器的一小部分。
现代x86上的负载通常为L1d高速缓存命中提供5个周期的 latency (从地址就绪到数据就绪),但它们还具有每时钟2个吞吐量。因此,即使没有任何高速缓存未命中,一个英特尔Sandybridge系列CPU或AMD K8 / Bulldozer / Zen的两个流水线负载执行单元中也可以一次同时运行多达10个负载。 (有了负载缓存器来跟踪缓存未命中的负载,整个无序的后端可以一次传输更多的负载。)
除非您正在谈论周围代码的特定上下文,否则不能说这样的CPU上的负载“花费”了5个周期。遍历一个链接列表,在该列表上您会遇到加载延迟的问题,因为下一次加载的地址取决于当前加载的结果。
遍历数组时,通常使用add
指令(或MIPS addui
)生成下一个指针,该指令具有1个周期的延迟。即使负载在简单的有序管道中具有5个周期的延迟,展开+软件流水线操作也可以使您每个时钟周期承受1个负载。
在流水线CPU上,性能不是一维的,您不能只将成本编号放在指令上并加以累加。您甚至可以在经典的有序MIPS上使用乘法指令的ALU指令上看到这一点:如果您不立即使用mflo
/ mhi
,那么乘以该指令的任何指令都会隐藏乘法延迟差距。
正如@duskwuff所指出的那样,像第一代MIPS这样的 a classic RISC 5-stage pipeline(获取/解码/执行/内存/回写)假定高速缓存命中具有1 /时钟的内存吞吐量和访问延迟L1d本身。但是MEM阶段为负载(包括EX阶段中的地址生成)提供了2个周期的等待时间。
我想他们也不需要存储缓冲区。更复杂的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)部分有这样的示例,但是我认为如果您需要等待添加WB,它们会低估失速周期。在bne / bgtz ID可以启动之前完成。)
因此,无论如何,这应该在标量有序MIPS I上每4个周期最多执行1次迭代,可以从EX转发到ID。每个bgtz
之前有3条指令+ 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--
放入分支延迟时隙中,将其尽可能远地执行循环分支的 next 。一个简单的bne
而不是bgtz
也会起作用;我们知道循环计数器从带符号的正数开始并在每次迭代中减少1,因此对非负数和非零数进行检查并不重要。
我不知道您使用的是哪种性能模型。如果它不是经典的5阶段MIPS,则上述内容无关紧要。