div 并不简单,它是最难计算的整数运算之一!它是在 Intel CPU 上进行微编码的,与 mov、add/sub 甚至 imul 不同,它们在现代 Intel 上都是单微指令。请参阅https://agner.org/optimize/ 获取指令表和微架构指南。 (有趣的事实:AMD Ryzen 没有对div 进行微编码;它只有 2 个微指令,因为它必须写入 2 个输出寄存器。Piledriver 和后来的版本也可以实现 32 位和 64 位除法 2 微指令。)
所有指令都解码为 1 或更多微指令(大多数程序中的大多数指令在当前 CPU 上为 1 微指令)。在 Intel CPU 上解码为 4 或更少微指令的指令被描述为“非微编码”,因为它们不使用特殊的 MSROM 机制来处理多指令指令。
没有将 x86 指令解码为 uops 的 CPU 使用简单的 3 阶段 fetch/decode/exec 循环,因此您问题的部分前提是没有意义的。再次,请参阅 Agner Fog 的微架构指南。
您确定要询问现代英特尔 CPU 吗?一些较旧的 CPU 是内部微编码的,尤其是非流水线 CPU,其中执行不同指令的过程可以以不同的顺序激活不同的内部逻辑块。 控制这一点的逻辑也称为微码,但它与流水线乱序 CPU 上下文中该术语的现代含义不同。
如果您正在寻找,请参阅 retrocomputing.SE 上的 How was microcode implemented in retro processors?,了解 6502 和 Z80 等非流水线 CPU,其中记录了一些微码内部计时周期。 p>
如何在现代 Intel CPU 上执行微编码指令?
当微编码的“间接微指令”到达 Sandybridge 系列 CPU 中的 IDQ 头部时,它会接管发布/重命名阶段,并从微码序列器 MS-ROM 向其提供微指令直到指令发出了所有的微指令,然后前端才能继续向乱序的后端发出其他微指令。
IDQ 是提供问题/重命名阶段的指令解码队列(它将微指令从前端发送到无序的后端)。它缓冲来自 uop 缓存 + 传统解码器的 uop,以吸收气泡和爆裂。这是David Kanter's Haswell block diagram 中的 56 uop 队列。 (但这表明微代码仅在队列之前被读取,这与英特尔对某些性能事件的描述不匹配1,或者对于运行数据相关的微指令数)。
(这可能不是 100% 准确,但至少可以作为大多数性能影响的心理模型2。对于性能可能还有其他解释到目前为止我们观察到的效果。)
这只发生在需要超过 4 个微指令的指令上;在普通解码器中需要 4 次或更少解码来分离 uops 并且可以正常发出的指令。例如xchg eax, ecx 在现代英特尔上是 3 微指令:Why is XCHG reg, reg a 3 micro-op instruction on modern Intel architectures? 详细介绍了我们可以弄清楚这些微指令实际上是什么。
微编码指令的特殊“间接”微指令在解码微指令缓存中占用一整行,即 DSB (potentially causing code-alignment performance issue)。我不确定他们是否只在从 uop 缓存和/或传统解码器 IDQ 提供问题阶段的队列中获取 1 个条目。无论如何,我编造了术语“间接uop”来描述它。它实际上更像是一条尚未解码的指令或指向 MS-ROM 的指针。 (可能一些微码指令可能是一对“普通”微指令和一个微码指针;这可以解释它自己占用一整条微指令缓存线。)
我很确定它们在到达队列头部之前不会完全扩展,因为一些微编码指令是可变数量的微指令,具体取决于寄存器中的数据。值得注意的是rep movs,它基本上实现了memcpy。事实上,这很棘手。根据对齐方式和大小使用不同的策略,rep movs 实际上需要进行一些条件分支。但它跳转到不同的 MS-ROM 位置,而不是不同的 x86 机器代码位置(RIP 值)。见Conditional jump instructions in MSROM procedures?。
Intel's fast-strings patent 还揭示了 P6 中的原始实现:第一个 n 复制迭代在后端进行预测;并给后端时间将ECX的值发送给MS。从那以后,如果需要更多,微码定序器可以发送正确数量的复制微指令,而无需在后端进行分支。也许处理几乎重叠的 src 和 dst 或其他特殊情况的机制毕竟不是基于分支,但 Andy Glew 确实提到了缺乏微码分支预测作为实现的一个问题。所以我们知道它们很特别。那是在 P6 天的时候; rep movsb 现在更复杂了。
根据指令的不同,它可能会或可能不会在整理出要执行的操作时耗尽无序后端的预留站(即调度程序)。 rep movs 对大于 96 字节的副本执行此操作不幸的是,在 Skylake 上(根据我对性能计数器的测试,将 rep movs 放在 imul 的独立链之间)。这可能是由于与常规分支不同的错误预测的微码分支。也许分支错过快速恢复对它们不起作用,所以直到它们退休才检测/处理它们? (有关更多信息,请参阅微码分支问答)。
rep movs 与mov 非常不同。普通的mov 和mov eax, [rdi + rcx*4] 一样是单个微指令,即使具有复杂的寻址模式。 mov 存储是 1 个微融合 uop,包括可以按任一顺序执行的存储地址和存储数据 uop,将数据和物理地址写入存储缓冲区,以便存储可以在指令后提交到 L1d从无序的后端退出并变得非投机性。 rep movs 的微码将包含许多加载和存储微指令。
脚注 1:
我们知道 Skylake 上有像 idq.ms_dsb_cycles 这样的表演活动:
[当微码序列器[原文如此] (MS) 忙时,由解码流缓冲区 (DSB) 启动的微指令被传送到指令解码队列 (IDQ) 时的周期]
如果微码只是输入 IDQ 前端的第三种可能的微指令来源,那将毫无意义。但是有一个事件的描述听起来像这样:
idq.ms_switches
[来自 DSB(解码流缓冲区)或 MITE(传统
解码管道)到微码排序器]
我认为这实际上意味着当问题/重命名阶段切换到从微码定序器而不是 IDQ(它保存来自 DSB 和/或 MITE 的 uops)时,它很重要。并不是说 IDQ 会切换其传入微指令的来源。
脚注 2:
为了测试这个理论,我们可以构建一个测试用例,在微编码指令之后有很多容易预测到冷 i-cache 行的跳转,并查看前端在跟踪缓存未命中和将微指令排队到大rep scasb 执行期间的 IDQ 和其他内部缓冲区。
SCASB 不支持快速字符串,因此速度非常慢,并且每个周期不会占用大量内存。我们希望它在 L1d 中命中,因此时间是高度可预测的。可能几个 4k 页面足以让前端跟踪大量 i-cache 未命中。我们甚至可以将连续的虚拟页面映射到同一个物理页面(例如,从用户空间在文件上使用mmap)
如果微码指令后面的 IDQ 空间可以在执行时被后面的指令填满,这就为前端在需要时从更多 i-cache 行获取更多空间留出了空间。然后,我们可以希望通过运行rep scasb 以及一系列跳转来检测总周期和/或其他性能计数器的差异。在每次测试之前,在包含跳转指令的行上使用clflushopt。
要以这种方式测试rep movs,我们可能会使用虚拟内存来将连续页面映射到同一个物理页面,再次为我们提供加载+存储的 L1d 命中,但 dTLB 延迟将难以控制。甚至可以在无填充模式下使用 CPU 启动,但这很难使用,并且需要自定义“内核”才能将结果放在可见的地方。
我非常有信心我们会在微编码指令接管前端时发现微指令进入 IDQ(如果它还没有满的话)。有一个 perf 事件
idq.ms_uops
[微码时将 Uops 传送到指令解码队列 (IDQ)
序列器 (MS) 正忙]
和其他 2 个事件,例如仅计算来自 MITE(旧版解码)的微指令或来自 DSB(微指令缓存)的微指令。英特尔对这些事件的描述与我对微码指令(“间接 uop”)如何接管发布阶段以从微码定序器/ROM 读取 uop 而前端的其余部分继续执行其向IDQ 的另一端,直到填满为止。