x86 乘以 3:IMUL 与 SHL + ADD
x86 Multiplication with 3: IMUL vs SHL + ADD
我用x86-64汇编开发了一个程序,需要通过同一个操作迭代很多次:
IMUL rdx, 3 # rdx is always different
但是,我需要使运行时更快,所以我想到了针对上面那条特定行的优化:
MOV rcx, rdx
SHL rdx, 1
ADD rdx, rcx
现在我问你们:这个修改会提高程序的运行时间(减少时钟),还是我应该坚持使用 IMUL
命令?
与lea rdx, [rdx + rdx*2]
相比,两者都很糟糕,使用scaled-index寻址模式得到总共*3
,这就是为什么编译器如果你要求他们编译像
这样的函数,他们将始终使用 LEA
long foo(long x){ return x * 3; }
(https://godbolt.org/z/6p4ynV)
LEA 是一种通过 x86 寻址模式 提供任意数字的方法,而无需 使用结果进行加载或存储,只需将其放入寄存器即可。
在所有现代 x86 CPU 上,LEA 是一个 uop。 唯一的问题是 它比替代方案好多少。 imul
也是 1 uop,但是 mov+shl+add 是3 为 front-end。 (这在所有主流和仍然相关的 low-power Intel/AMD 中都是如此。请参阅 https://agner.org/optimize/)64 位 imul
在一些较旧的微体系结构上特别慢,例如 Bulldozer-family 和 Silvermont/Goldmont,或者尤其是较旧的 Atom。
在 AMD CPU (Bulldozer/Ryzen) 上,它有一个缩放索引,因此它是一个 "complex" LEA 并且有 2 个周期延迟(对比 Ryzen 上 imul
的 3 个,或者更多在 Bulldozer-family 上更糟,其中 64 位 imul
速度较慢且未完全流水线化)。在 Ryzen 上,这个 LEA 仍然有 2-per-clock 的吞吐量。
在英特尔 CPU 上,它只有 2 个组件(一个 +
),因此它是一个 "simple" LEA,具有 1 个周期延迟并且可以 运行 具有 2-per-clock 吞吐量。 因此与一条 shl
指令的成本大致相同,但 运行 在不同的端口上。
(或者在 Ice Lake 上,4-per-clock 因为他们将 LEA 单元添加到其他 2 个整数 ALU 端口。所以它和 Ice Lake 上的 add
一样便宜。)
你只想要 mov
; shl
; sub
或 add
当您的乘数为 2^n +- 1 时 n > 3
。那么值得考虑 imul
以在延迟和 front-end 吞吐量成本之间进行权衡。
通过移动原始寄存器,即使没有 mov
-elimination 的 CPU(在 IvyBridge 和 Ryzen 之前)也可以 运行 您的 mov/shl/add 序列具有 2 个周期延迟关键路径长度。
也相关: 有一些关于 *3
与 LEA 优化的问题的详细信息。
其他相关:
我用x86-64汇编开发了一个程序,需要通过同一个操作迭代很多次:
IMUL rdx, 3 # rdx is always different
但是,我需要使运行时更快,所以我想到了针对上面那条特定行的优化:
MOV rcx, rdx
SHL rdx, 1
ADD rdx, rcx
现在我问你们:这个修改会提高程序的运行时间(减少时钟),还是我应该坚持使用 IMUL
命令?
与lea rdx, [rdx + rdx*2]
相比,两者都很糟糕,使用scaled-index寻址模式得到总共*3
,这就是为什么编译器如果你要求他们编译像
long foo(long x){ return x * 3; }
(https://godbolt.org/z/6p4ynV)
LEA 是一种通过 x86 寻址模式 提供任意数字的方法,而无需 使用结果进行加载或存储,只需将其放入寄存器即可。
在所有现代 x86 CPU 上,LEA 是一个 uop。 唯一的问题是 它比替代方案好多少。 imul
也是 1 uop,但是 mov+shl+add 是3 为 front-end。 (这在所有主流和仍然相关的 low-power Intel/AMD 中都是如此。请参阅 https://agner.org/optimize/)64 位 imul
在一些较旧的微体系结构上特别慢,例如 Bulldozer-family 和 Silvermont/Goldmont,或者尤其是较旧的 Atom。
在 AMD CPU (Bulldozer/Ryzen) 上,它有一个缩放索引,因此它是一个 "complex" LEA 并且有 2 个周期延迟(对比 Ryzen 上 imul
的 3 个,或者更多在 Bulldozer-family 上更糟,其中 64 位 imul
速度较慢且未完全流水线化)。在 Ryzen 上,这个 LEA 仍然有 2-per-clock 的吞吐量。
在英特尔 CPU 上,它只有 2 个组件(一个 +
),因此它是一个 "simple" LEA,具有 1 个周期延迟并且可以 运行 具有 2-per-clock 吞吐量。 因此与一条 shl
指令的成本大致相同,但 运行 在不同的端口上。
(或者在 Ice Lake 上,4-per-clock 因为他们将 LEA 单元添加到其他 2 个整数 ALU 端口。所以它和 Ice Lake 上的 add
一样便宜。)
你只想要 mov
; shl
; sub
或 add
当您的乘数为 2^n +- 1 时 n > 3
。那么值得考虑 imul
以在延迟和 front-end 吞吐量成本之间进行权衡。
通过移动原始寄存器,即使没有 mov
-elimination 的 CPU(在 IvyBridge 和 Ryzen 之前)也可以 运行 您的 mov/shl/add 序列具有 2 个周期延迟关键路径长度。
也相关:*3
与 LEA 优化的问题的详细信息。
其他相关: