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

Arb*_*ole 1 x86 assembly cpu-architecture riscv

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

add x1,x2,x3
Run Code Online (Sandbox Code Playgroud)

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

mov x1,x2
add x1,x3
Run Code Online (Sandbox Code Playgroud)

该模式对于 x86 中的其他基本指令(例如、和 )src1 <- src1 op src2似乎很常见。然而,x86 确实有浮点数的eg 。andorsubdest <- src1 op src2add

是双指令模式mov x1,x2op x1,x3; 通常将宏融合到单个微操作中?或者,对于这些操作来说,独立目标是如此不常见,以至于 x86 架构不会费心在单个 uop 中允许它?如果是这样,禁止独立目的地会带来什么效率?

Pet*_*des 7

几乎重复了x86 cpu 有什么样的地址指令?这解释了机器代码的原因(以及一般情况的一些例外)。

如果是这样,禁止独立目的地会带来什么效率?

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

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

您可以说,作为 CISC ISA,x86 将其额外的编码空间用于内存源操作数的可能性,而不是单独的目标。尽管这只是某种程度的真实,因为只有 2 位编码寄存器与[寄存器]间接与[reg+disp8]与[reg+disp32]。其余空间不存在,因为典型指令只有 2 个字节长,即操作码 + modrm。(加上前缀、立即数和/或寻址模式的额外字节)。

有趣的是,16 位与 ARM Thumb 的长度相同,ARM Thumb 做出了相同的选择,主要是 2 操作数编码,因为这就是您保持指令较小的方式,但有时需要更多指令。在最初的 8086(尤其是具有半宽总线的 8088)上,取码是主要瓶颈,无论指令数量多少,节省代码字节通常都会提高性能。

x86 机器代码当时已经一成不变,我们仍然坚持使用它。这对于当今的 CPU 来说非常不方便,32 位模式下的 VEX 和 EVEX 编码被硬塞在其他指令的无效编码上;它完全是一团糟,而且解码速度非常慢且耗电。例如,Intel CPU 有一个单独的管道阶段,只是为了在将指令提供给解码器之前查找指令长度/边界。这就是为什么现代 CPU 具有解码的 uop 缓存,以避免在“热”代码区域中重新解码,以及为什么由于这些长管道而需要良好的分支预测。

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


另请注意,ADDPDADDSD是 2 操作数 SSE 指令。AVX 中新增了同一指令的 3 操作数非破坏性目标编码,称为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+ 分支指令,如 AND+分支或 DEC+分支。) 当代 x86 处理器中的指令融合是什么?还涵盖了内存源 CISC 指令中加载+ALU 微指令的微融合。

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

然而,mov-elimination 对提高前端吞吐量或保持较小的无序窗口没有帮助。对于管道的其余部分,MOV 的成本与 NOP 相同。

Haswell 到 Skylake 的前端宽度与后端 ALU 执行单元的数量相同。即使使用 Ice Lake 和 Zen(更宽的前端,仍然“仅”4 个整数 ALU 执行单元),非消除mov很少会成为瓶颈。大多数代码偶尔包含存储或非微融合加载微指令。

  • 我喜欢你将 AArch64 称为“漂亮干净的设计”。我当然是它的超级粉丝,但它是我见过的最像 CISC 的 RISC 指令集。 (2认同)
  • @fuz:是的,但这并不能阻止它以重要的方式保持干净(易于并行解码和管道)。设计人员并没有让象牙塔 RISC 的纯度阻止他们制作具有良好代码密度的优秀 ISA。对于带有寄存器重命名的现代超标量 CPU 来说,标志依赖性是一个已解决的问题,并且在其奇特的立即数编码方式上花费一些额外的晶体管对于代码密度非常有好处,特别是对于位模式常量。在某些方面,它就像 Agner Fog 的 [ForwardCom 论文 ISA](https://forwardcom.info/):汲取 RISC 和 CISC 的最佳部分 (2认同)

Ros*_*dge 5

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

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

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

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

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