【问题标题】:Why does x86 commonly not allow a destination register that is not the first source register?为什么 x86 通常不允许不是第一个源寄存器的目标寄存器?
【发布时间】:2020-12-24 14:10:29
【问题描述】:

在 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,x2op x1,x3;通常将宏融合到单个微操作中?或者对于这些操作来说,独立目的地是如此罕见,以至于 x86 架构不会在单个 uop 中允许它?如果是这样,不允许独立目的地提供什么效率?

【问题讨论】:

  • x86 被设计为双操作数架构。每条指令都有两个操作数,编码不允许更多。最近在 AVX 中添加了三操作数指令,但这些指令并未广泛使用。
  • @ThomasJager 甚至 SSE 也是严格的双操作数指令集。也许OP在谈论x87?不确定他指的是哪三个操作数 FP 操作。
  • @fuz :一些指令的三种操作数形式被添加到后来的处理器中,例如imul。但是编码将您限制在可以用作操作数的范围内。
  • @ArborealAnole ADDPSADDSD 像任何其他普通 x86 指令一样采用两个操作数。您可能是指VADDPSVADDSD? VEX 前缀允许它们采用第三个操作数。
  • This article 你可能会感兴趣。

标签: assembly x86 cpu-architecture riscv


【解决方案1】:

几乎是 What kind of address instruction does the x86 cpu have? 的副本,它解释了机器代码原因(以及一般情况的一些例外情况)。

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

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

x86 使用紧凑的可变长度指令编码,evolved as a 2-operand ISA out of 8-bit 8080 或多或少是一个 1 操作数 ISA,其中 most opcodes implied one of the operands(通常是累加器)。

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

有趣的事实是,16 位与 ARM Thumb 的长度相同,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; 以便解决最常见指令的问题。 Using LEA on values that aren't addresses / pointers? 看看 x86-64 优化的编译器输出。

x86 微架构在实践中不会对 mov + op 进行宏融合,尽管这在理论上是可能的。实际上,编译器确实必须使用大量的mov reg,reg 指令,但每条 ALU 指令明显少于 1 条。硬件供应商在解码时还没有开始寻找融合机会。目前,他们只将 cmp/test + branch 融合到一个 uop 中。 (或on Intel Sandybridge-family,还有其他 ALU+分支指令,如 AND+branch 或 DEC+branch。)What is instruction fusion in contemporary x86 processors? 还涵盖了内存源 CISC 指令中 load+ALU 微指令的微融合。

MOV elimination at issue/rename time 确实使 MOV+ALU 对的关键路径仍然只有 1 个周期延迟。(尽管您有时可以通过拥有关键路径来获得相同的延迟优势使用原始的,一些较短延迟或独立的 dep 链使用副本。但通常这需要循环展开。)

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

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

【讨论】:

  • 我喜欢您将 AArch64 称为“漂亮干净的设计”的方式。我当然是它的忠实粉丝,但它是我见过的最像 CISC 的 RISC 指令集。
  • @fuz:是的,但这并不能阻止它以重要的方式变得干净(易于解码和并行流水线)。设计者并没有让象牙塔 RISC 纯度阻止他们制作具有良好代码密度的良好 ISA。 FLAGS 依赖性对于具有寄存器重命名的现代超标量 CPU 来说是一个已解决的问题,并且在其花哨的立即数编码方式上花费一些额外的晶体管对于代码密度非常有用,尤其是对于位模式常量。在某些方面,它就像 Agner Fog 的 ForwardCom paper ISA:充分利用 RISC 和 CISC
  • 移动消除是一种相当常见的微架构优化。
  • @PaulA.Clayton:是的,这有助于延迟,所以值得一提。但这对前端吞吐量没有帮助,后端整体 ALU 吞吐量通常不是瓶颈。
  • 一个问题是为什么在 AMD 将 x86 切换到 x86-64 之后,今天仍然“坚持”。这是一种不同的操作模式,需要不同的解码规则,所以你可以想象一个戏剧性的清理。相反,他们只做了一些小的改动,为新前缀或类似的东西留出空间。鉴于市场限制,这可能是有道理的,但可能值得注意的是,它并不是像 8086 ISA 那样直接通过(大概在 16 -> 32 过渡期间还有其他类似的时刻)。
【解决方案2】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2020-09-14
    • 1970-01-01
    • 2016-12-28
    • 2013-04-21
    • 2014-11-19
    • 2019-04-16
    • 2012-02-17
    • 2012-02-10
    • 2019-01-02
    相关资源
    最近更新 更多