几乎是 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 这样干净利落的设计?
另请注意,ADDPD 和 ADDSD 是 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。