组装为什么 lea 快?

问题描述

我和我的教授进行了交谈,他说:

leaq (%rax,%rax,8)

比:

imulq $9,%rax

我问他为什么(在这两种情况下,我们用几乎相同的数字进行乘法运算),他说我们不会讨论这个问题。

有人可以帮助我简单地理解为什么 leaq 通常很快吗?

评论中提出的一个问题是:

imulq $9,%rax

比执行 2 个命令更快,一个是左移,另一个添加一个 %rax(我们之前可以将其保存在寄存器中)

为什么?

解决方法

lea(加载有效地址)是执行指针运算的常见操作的一种方式。指令如何引用其操作数称为其 addressing modelea 支持 scaledbase plus index plus offset 寻址模式(以及其他)。

address = base address + index * scaling + offset

其中缩放值可以是 2 的几个幂 (1,2,4,8) 之一。这些值对于字节、字符、整数、指针等数组很有用。它不能编码或执行与任意值的乘法。在硬件中,这几个选项可以通过几个多路复用器实现,延迟周期的一小部分。

另一方面,乘法指令通过一个乘法电路,该电路可以将两个任意全宽(64 位)操作数相乘。这是一个复杂度明显更高的操作。即使并行使用多个全宽加法器,它的延迟也是全宽加法的大约 6 倍 (log n)(尽管该设计可能包含优化,使其能够更快地乘以更简单的值)。

,

LEA 只是 a shift-and-add instruction,带有 2 位移位计数作为机器编码的一部分。这比完整的 64 位乘法器在硬件中构建要便宜得多,这也是为什么 CPU 可以轻松拥有多个可以处理 LEA 微指令的执行单元。 (优于 1/clock 的吞吐量)。

请注意,LEA 延迟仅为 1 个周期,仅适用于足够简单的寻址模式(在 Ice Lake 之前)。在 Intel SnB 系列 CPU 上,没有任何具有 2 个周期延迟的 uops,而具有 3 个组件(两个 + 操作)的 LEA 具有 3 个周期延迟。显然,英特尔无法或没有将足够的门延迟用于在 Ice Lake 之前的单个 ALU 周期中进行 2 次添加(或 3-> 2 次减少和 1 次添加)。

但是,是的,像问题中的那个(没有位移)更简单的 LEA 在 SnB 系列上是 1 个周期的延迟和 2/clock 的吞吐量,“慢” LEA 只在端口 1(上的唯一执行端口)上运行可以运行整数 uops 且延迟不为 1 的 SnB 系列。)

Ice Lake 总是 1c 延迟,1 uop。包括缩放索引(移位计数!= 0)的寻址模式为 2/时钟吞吐量,否则为 4/时钟。 (即使对于像 lea 1(%rax,%rcx),%edx 这样的 3 组件操作,这在 Skylake 或 Zen 上也是“慢 LEA”)。

在 AMD 上,lea 是 1 或 2 个周期的延迟,对于慢速 LEA,吞吐量减少类似(端口更少)。并且要快的条件更严格:1 以外的比例因子使它变慢。但是 Zen 仍然有 2 个可以处理“慢” LEA 的执行单元,4 个用于快速 LEA。 https://uops.info/ https://agner.org/optimize/


imulq $9,%rax 比执行 2 个命令快,一个是左移,另一个是添加一个 %rax(我们之前可以将其保存在寄存器中)

imul $9,%rax 是 AMD 自 Zen 以来的 1 uop、3c 延迟、1/clock 吞吐量,自 Nehalem 以来的 Intel。 (https://uops.info/)。较旧的 CPU 具有更高的延迟,尤其是对于 64 位操作数大小。

shl $3,%rax / add %rcx,%rax 是前端的 2 uop,但只有 2 个周期的延迟。 (在此之前的某个地方可能还有一个额外的移动,用于第三个 uop)。

然而,任何体面的编译器都会使用 lea (%rax,%rax,8),%rax 来代替 (a*9 = a + a*8):1 uop,英特尔上的 1c 延迟,2/clock 吞吐量,因此它在任何方面都不会变差,而且在很多方面都更好。 (或者在最坏的情况下,由于缩放索引,AMD 上有 2 个周期的延迟,但这仍然比 imul 好。)

当您查看单个指令或短序列时,性能不是一维的,而是 3 维的:前端 uops、后端端口和关键路径的延迟。没有单数成本可以跨指令相加以找出指令块需要多长时间;超标量乱序执行的全部意义在于找到指令级并行性,其存在取决于指令如何使用彼此的结果。 (但有时您可以说一个序列在各方面至少与另一个序列一样好,如果它在所有现有 CPU 的所有 3 个方面都相同或更好。)