【发布时间】:2016-10-28 07:39:08
【问题描述】:
我记得在优化 x86 以提高速度时,通常要避免读取-修改-写入指令。也就是说,你应该避免像add [rsi], 10 这样的东西,它会增加存储在rsi 中的内存位置。建议通常是将其拆分为 read-modify 指令,然后是 store,因此类似于:
mov rax, 10
add rax, [rsp]
mov [rsp], rax
或者,您可以使用显式加载和存储以及 reg-reg 添加操作:
mov rax, [esp]
add rax, 10
mov [rsp], rax
对于现代 x86,这仍然是合理的建议(曾经是这样吗?)?1
当然,如果内存中的值被多次使用,RMW 是不合适的,因为您会产生冗余的加载和存储。我对一个值只使用一次的情况感兴趣。
基于在 Godbolt 中的探索,所有 icc、clang 和 gcc prefer 使用单个 RMW 指令来编译类似:
void Foo::f() {
x += 10;
}
进入:
Foo::f():
add QWORD PTR [rdi], 10
ret
所以至少大多数编译器似乎认为 RMW 没问题,因为该值只使用一次。
有趣的是,当增量值是全局而不是成员时,各种编译器不同意,例如:
int global;
void g() {
global += 10;
}
在这种情况下,gcc 和 clang 仍然是单个 RMW 指令,而 icc prefers 是带有显式加载和存储的 reg-reg add:
g():
mov eax, DWORD PTR global[rip] #5.3
add eax, 10 #5.3
mov DWORD PTR global[rip], eax #5.3
ret
也许这与RIP 相对寻址和微融合限制有关?但是,icc13 仍然对-m32 做同样的事情,所以可能更多的是与需要 32 位位移的寻址模式有关。
1我故意使用模糊的术语现代 x86 基本上是指英特尔和 AMD 笔记本电脑/台式机/服务器芯片的最后几代。
【问题讨论】:
-
您查看过 Agner Fog 的微架构指南吗? RIP-relative 寻址模式在某种程度上可能很特别,但我认为它们有时可以微融合。但通常,IIRC,内存目标操作最多与单独的加载/修改/存储指令相同数量的微指令。 IIRC 对于 Broadwell/Skylake 上的
adc而言并非如此。 -
icc 可能偏爱单独的指令,因此它们都可以是单指令。 Godbolt的icc13是旧的;许可问题使得提供更新版本是否合法尚不清楚。它可能正在优化没有 uop 缓存的 CPU 上的解码速度,其中不是解码组中第一个的多 uop 指令将降低解码器吞吐量。 (OTOH,是解码组中第一个的多微指令增加了解码吞吐量,因为 Core2/Nehalem 解码器最多可以解码 4-1-1-1,这与 SnB 系列不同)。
-
我稍后可能会将其转换为答案,但现在这只是一个快速评论,无需查找任何内容,只是略过问题。
-
icc 可能试图在同一条指令中避免 disp32 + immediate(请参阅我对问题的编辑;它与 -m32 做同样的事情)。 disp + imm 对微融合 IIRC 有影响。或者至少可能需要 uop 缓存中的多个条目,即使就 ROB 而言它实际上是单个融合域 uop。虽然我认为这只是 disp32+imm32 的问题,而不仅仅是 disp32 + imm8。无论如何,我可能会在 Agner 的手册中挖掘这些东西,但您可以根据这些 cmets 作为起点自己挖掘它,并将其写成一个不错的答案。
-
Darek Mihocka 不久前用各种架构对此进行了测试;你可以在他的博客here 上读到他的面条(关于 Pentium 4 和 x86 的一般问题的一系列引人入胜的文章的一部分)。不同的编译器有不同的代码生成策略,但原因是性能调优复杂。没有一刀切的策略。如果您真的关心,您必须对您的目标架构进行基准测试。
标签: assembly optimization x86 intel