现代 x86 CPU 能否实现理想的无序执行?

Can modern x86 CPUs do ideal out of order execution?

提问人:Joseph Garvin 提问时间:11/17/2023 最后编辑:Peter CordesJoseph Garvin 更新时间:11/17/2023 访问量:69

问:

我对无序执行的心智模型是将其视为指令流上的滑动窗口,如果窗口中有任何指令已准备好(它们的输入已经计算出来),即使流中还有其他指令,它们也可以立即启动, 前提是 CPU 资源可用。

但是,我试图理解在彼此不依赖的指令之间排序的影响。假设我有两组三条指令,每组指令形成独立的依赖链和 .这些指令都不会更改控制流。由于每个集合中的指令形成依赖链,因此集合中的指令不能无序执行(无效),但由于链是独立的,因此它们可以相互交错(有效)。进一步假设所有 6 条指令都位于滑动窗口内,并且都在缓存中。这六条指令的所有有效类型是否都具有相同的性能,或者某些指令会比其他指令更快?有 N 链的 M 指令?A,B,CX,Y,ZC,B,AA,X,B,Y,C,Z

我的怀疑是否定的,因为在硬件中执行理想的调度似乎很复杂,但也许在实践中有足够好的启发式方法真正的 CPU 使用?

假设某些排序比其他排序更快,那么如何使用性能计数器或其他工具来检测代码的运行速度是否比排序错误时慢?有没有办法看出热回路组件中的排序是坏的?与更常见的嫌疑人(如分支误判和缓存未命中)相反。

汇编 x86 CPU 架构 低级 超标量

评论


答:

4赞 Peter Cordes 11/17/2023 #1

你要问的概念是“调度”,要么由编译器静态(asm 的程序顺序)或在 CPU 中动态(由“调度器”,又名 RS = Reservation Stations)。

请参阅 x86 uops 究竟是如何调度的? - 对于每个执行端口,首先使用最旧的 uops,不检测哪些 uops 是长关键路径依赖链的一部分。通常,这主要是在循环的几次迭代后自然发生的,因为未执行的 uop 是循环携带的依赖链的一部分。

对于像 integer 这样的东西,您需要安排 for before ,因为它是 2 秒链(3 个周期延迟,1 个/周期吞吐量)的一部分。如果 是静态的,则按程序顺序(并且所有 5 个输入都在寄存器中准备就绪),它将首先执行。因此,较长的依赖链要到下一个周期才能启动,从而导致关键路径长度为 。如果整个表达式的关键路径延迟是较长的 dep 链的一部分,这是一个整体瓶颈(但不知何故,我们仍然同时准备好了所有 5 个输入,或者至少是 d、e、a 和 b),那么首先静态调度关键路径 uops 会有所帮助,避免一个具有整数乘数的执行端口的资源冲突。a*b*c + d*eimula*bd*eimuld*eimul1(resource conflict) + 3+3(imuls) + 1(add)

(我选择整数乘法作为更简单的例子,因为现代 x86 CPU 仍然只有一个执行端口可以处理它。Intel CPU 上的 SIMD 随机播放是另一个常见的例子,至少对于一些无法在 Ice Lake 的端口 1 上运行或具有 512 位矢量宽度的随机播放。


uops 在 issue/rename/alloc 期间分配给端口(当它们从有序前端移动到无序后端时,RS 和 ROB = ReOrder Buffer)。这是针对问题组中的所有 uop(例如,在最近的 x86 上为 4 到 6)并行完成的,具体取决于周期开始时每个端口未执行的 uops 数量。在循环的前几次迭代中,调度可能是次优的;请参阅x86_64已使用的端口而不是未使用的端口上安排的 Haswell 指令 - 英特尔使用一些平局启发式方法在队列长度接近时将 uops 分发到端口,而不是在同一周期内检查 uops 之间的端口限制。


有关 CPU 的框图,请参阅


如何使用性能计数器或其他工具来检测代码的运行速度是否比排序错误时要慢?

一般很难检测。如果您已经针对循环携带的依赖链的预期延迟瓶颈运行了数字,但实际上它的运行速度比这慢,并且没有缓存未命中或前端的停顿,那么 uop 调度是值得关注的。

在 Intel CPU 上,计算有 uops 准备从前端发出的周期(IDQ = 指令解码队列),但由于后端停滞,它们没有。idq_uops_not_delivered.cycles_fe_was_ok


展开以隐藏延迟(通过重叠多个 dep 链而不是一个更长的 dep 链)对于高延迟操作(如浮点数学,如点积或求和数组)非常重要。