为什么 x86 通常不允许不是第一个源寄存器的目标寄存器



在RISC-V中,可以使用指令执行整数运算Regs[x1] <- Regs[x2]+Regs[x3]

add x1,x2,x3

在 x86 中,相同的操作显然需要两条指令,

mov x1,x2
add x1,x3

src1 <- src1 op src2模式似乎在 x86 中的其他基本指令中很常见,例如andorsub。但是,x86 确实具有dest <- src1 op src2例如浮点adds。

双指令模式是否mov x1,x2;op x1,x3;通常宏融合到单个微操作中?还是独立目标对于这些操作如此罕见,以至于 x86 架构懒得在单个 uop 中允许它?如果是这样,不允许独立目的地提供什么效率?

几乎是x86 CPU具有哪种地址指令的副本?这解释了机器代码的原因(以及一般情况的一些例外)。

如果是这样,不允许独立目的地提供什么效率?

只是代码大小。 它使其他一切都变得更糟,这就是为什么所有现代高性能设计都提供 3 操作数指令,以及如果他们从头开始重新架构 x86-64 以提高性能,任何人都会怎么做。

x86 使用紧凑的可变长度指令编码,并从 8 位 8080 演变为 2 操作数 ISA,这或多或少是一个 1 操作数 ISA,其中大多数操作码暗示其中一个操作数(通常是累加器)。

可以说,作为CISC ISA,x86在内存源操作数的可能性上使用其额外的编码空间,而不是在单独的目的地上使用。 虽然这只是正确的,因为只有 2 位编码寄存器与 [寄存器] 间接 vs. [reg+disp8] 与 [reg+disp32]。 其余的空间不存在,因为典型的指令只有 2 个字节长,操作码 + modrm。 (加上前缀、即时和/或额外字节的寻址模式)。

有趣的是,16 位与 ARM Thumb 的长度相同,后者选择了大部分 2 操作数编码,因为这就是您保持指令小的方式,而代价是有时需要更多指令。 在最初的 8086(尤其是带有半宽总线的 8088)上,代码获取是主要瓶颈,无论指令数量如何,节省代码字节通常都能提供性能。

x86机器代码当时是一成不变的,我们仍然坚持下去。 对于今天的 CPU 来说,这是非常不方便的,32 位模式下的 VEX 和 EVEX 编码与其他指令的无效编码硬塞进去;这完全是一团糟,解码非常慢+耗电。 例如,英特尔 CPU 具有单独的流水线阶段,只是为了在将指令馈送到解码器之前找到指令长度/边界。 这就是为什么现代CPU具有解码uop缓存,以避免在"热"代码区域中重新解码,以及为什么由于这些长管道而需要良好的分支预测。

任何抛弃 2 操作数编码以腾出更多空间的小修都会引发一个问题,为什么要保留任何遗留包袱,为什么不从头开始? 然后,为什么要是x86-64,为什么不是像AArch64这样的漂亮干净的设计呢?


另请注意,ADDPDADDSD是 2 操作数的 SSE 指令。 同一指令的 3 操作数无损目的地编码是 AVX 的新功能,称为VADDPD/VADDSD

<小时 />

MOV + ADD 的效率

mov/add(和移位)可以用lea来完成,例如lea eax, [rdi + rsi*4]实现return x + y*4;以便解决最常见指令的问题。 对不是地址/指针的值使用 LEA? 查看 x86-64 优化的编译器输出。

实际上,x86 微架构不会宏融合 MOV + OP,尽管这在理论上是可能的。 在实践中,编译器确实必须使用大量的mov reg,reg指令,但每个 ALU 指令明显少于 1 条。 还不够,硬件供应商还没有开始在解码时寻找这种融合机会。 目前,他们只将 cmp/test + 分支融合到单个 uop 中。 (或者在Intel Sandybridge家族上,还有其他ALU + branch指令,如AND + branch或DEC+branch。 什么是当代 x86 处理器中的指令融合?还涵盖了存储器源CISC指令中负载+ALU uops的微融合。

在问题/重命名时消除 MOV 确实使 MOV+ALU 对在关键路径上仍然只有 1 个周期延迟。(尽管有时可以通过让关键路径使用原始路径来实现相同的延迟优势,并且一些延迟较短或独立的 dep 链使用副本。 但这通常需要循环展开。

但是,mov消除无助于提高前端吞吐量,也无助于缩小无序窗口。 对于管道的其余部分,MOV 的成本与 NOP 相同。

Haswell通过Skylake的前端宽度与后端的ALU执行单元数量相同。 即使有 Ice Lake 和 Zen(前端更宽,仍然"只有"4 个整数 ALU 执行单元),未消除的mov也很少会成为瓶颈。 大多数代码包括偶尔存储或非微熔化负载 uop。

Intel 8086 的两个操作数设计的最初动机是保持指令解码器的简单性,其中目标和第一个操作数必须是相同的寄存器。 8086只有27,000个晶体管。 英特尔没有晶体管预算来实现三操作数指令集。

虽然 x86 指令集经常受到批评,需要需要大量晶体管的复杂解码器,但这仅适用于您尝试尽可能快地解码现代 x86 指令集的情况。 正如最初的8086设计所示,它从根本上不需要大量的晶体管来解码基本指令集。

在设计 8086 时,双操作数指令集并没有什么不寻常的。 它的主要竞争对手68000也有一个双操作数指令集,IBM大型机也是如此。 这实际上是对8位微处理器设计的改进,如Intel 8080,其晶体管预算要小得多,通常实现一个操作数指令集,其中目标和第一个操作数始终是累加器。

虽然双操作数指令集允许更紧凑的编码,但这不是目标。 英特尔做出的一些设计决策简化了解码,实际上增加了代码大小。 指令前缀占用了整个字节,以有效地向指令编码添加几个位。 然而,通过将它们视为在处理器中设置隐藏内部标志的单字节指令,它们很容易实现。 很少使用的单字节XCHG指令可能被设计为实现NOP指令(XCHG AX,AX)的廉价方式而设计,尽管设计人员也可能只是认为它会经常使用以证明单字节编码的合理性。 无论哪种方式,如果将此操作码空间用于它们,则还有许多其他更常用的操作可以产生更紧凑的代码。

如果您以当今的晶体管预算从头开始设计指令集,您可能会设计一个三操作数指令集。 然而,在晶体管数量仍然令人担忧的地方,您确实看到了相对现代的设计,例如仅支持两个操作数的 8 位 AVR 指令集。

相关内容

最新更新