现代CPU中同时发生了很多很多事情.当然,任何需要内存访问结果的东西都无法继续,但可能还有很多事情要做.假设以下C代码:
double sum = 0.0;
for (int i = 0; i < 4; ++i) sum += a [i];
if (sum > 10.0) call_some_function ();
Run Code Online (Sandbox Code Playgroud)
并假设读取数组一个档位.由于读取[0]档,加法和+ = a [0]将停止.但是,处理器继续执行其他指令.就像增加i一样,检查i <4,循环和读取[1].这也停止了,第二个加法和+ = a [1]失速 - 这次因为既没有正确的sum值也没有值a [1]都知道,但事情仍在继续,最终代码到达语句"if(总和> 10.0)".
此时处理器不知道总和是多少.然而,它可以根据先前分支中发生的情况猜测结果,并开始推测性地执行函数call_some_function().所以它继续运行,但要小心:当call_some_function()将内容存储到内存时,它还没有发生.
最终读取[0]成功,许多周期后.当发生这种情况时,它将被添加到sum,然后a [1]将被添加到sum,然后a [2],然后a [3],然后比较和> 10.0将正确执行.那么分支的决定将变得正确或不正确.如果不正确,call_some_function()的所有结果都将被丢弃.如果正确,call_some_function()的所有结果都会从推测结果变为实际结果.
如果停滞时间过长,处理器最终将无法完成任务.它可以轻松处理四个添加和一个无法执行的比较,但最终它太多了,处理器必须停止.但是,在超线程系统上,你有另一个可以继续快速运行的线程,而且速度更快,因为没有其他人使用核心,所以整个核心仍然可以继续做有用的工作.
现代乱序处理器有一个重新排序缓冲区(ROB),它跟踪所有运行中的指令并保持它们的程序顺序。一旦 ROB 头部的指令完成,它就会从 ROB 中清除。现代 ROB 的大小约为 100-200 个条目。
同样,现代 OoO 处理器有一个加载/存储队列,用于跟踪所有内存指令的状态。
最后,已获取并解码但尚未执行的指令位于所谓的“发布队列/窗口”(或“保留站”,具体取决于设计者的术语和模态微架构中的一些差异)中与这个问题无关)。位于发出队列中的指令具有它们所依赖的寄存器操作数的列表以及它们的操作数是否“忙”。一旦所有寄存器操作数不再繁忙,指令就准备好执行并请求“发出”。
发布调度程序从准备好的指令中挑选并将它们发布到执行单元(这是无序的部分)。
让我们看一下以下顺序:
addi x1 <- x2 + x3
ld x2 0(x1)
sub x3 <- x2 - x4
Run Code Online (Sandbox Code Playgroud)
正如我们所看到的,“sub”指令取决于先前的加载指令(通过寄存器“x2”)。加载指令将被发送到内存并在高速缓存中丢失。它可能需要 100+ 个周期才能返回结果并将结果写回 x2。同时,子指令将被放入发布队列,其操作数“x2”被标记为忙。它会坐在那里等待非常非常长的时间。ROB 将很快填满预测的指令,然后停止。整个核心将会停止运转并转动它的拇指。
一旦负载返回,它就会写回“x2”,并将此事实广播到问题队列,子设备会听到“x2 现在准备好了!” sub 终于可以继续执行,ld 指令终于可以提交,ROB 将开始清空,以便可以获取新指令并将其插入到 ROB 中。
显然,这会导致管道空闲,因为大量指令将被备份以等待负载返回。对此有几个解决方案。
一种想法是简单地将整个线程切换为新线程。简而言之,这基本上意味着刷新整个管道,将线程的 PC(指向加载指令)和提交的寄存器文件的状态(在 add 指令结束时)存储到内存中。加载)。在缓存未命中的情况下调度新线程需要做大量工作。恶心。
另一种解决方案是同时多线程。对于2路SMT机器,您有两台PC和两个架构寄存器文件(即,您必须为每个线程复制架构状态,但您可以共享微架构资源)。通过这种方式,一旦您获取并解码了给定线程的指令,它们对后端来说是相同的。因此,虽然“sub”指令将永远在问题队列中等待加载返回,但另一个线程可以继续前进。当第一个线程逐渐停止时,可以将更多资源分配给第二个线程(获取带宽、解码带宽、发出带宽等)。以这种方式,管道通过毫不费力地用第二个线程填充来保持忙碌。