assembly 在C++编译的汇编中使用莱亚传递参数优于MOV的优点

piztneat  于 2023-08-06  发布在  其他
关注(0)|答案(2)|浏览(113)

我正在试验在编译C代码时将参数传递给函数的方式。我尝试使用x64 msvc 19.35/latest编译器编译以下C代码,以查看生成的程序集:

#include <cstdint>

void f(std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);

void test()
{
    f(1, 2, 3, 4);
}

字符串
得到了这个结果:

void test(void) PROC
        mov     edx, 2
        lea     r9d, QWORD PTR [rdx+2]
        lea     r8d, QWORD PTR [rdx+1]
        lea     ecx, QWORD PTR [rdx-1]
        jmp     void f(unsigned int,unsigned int,unsigned int,unsigned int)
void test(void) ENDP


Result on godbolt.org
我不明白的是为什么编译器选择使用lea而不是简单的mov。我理解lea的机制,以及它如何在每个寄存器中产生正确的值,但我希望有更简单的东西:

void test(void) PROC
        mov     ecx, 1
        mov     edx, 2
        mov     r8d, 3
        mov     r9d, 4
        jmp     void f(unsigned int,unsigned int,unsigned int,unsigned int)
void test(void) ENDP


此外,从我对现代CPU如何工作的一点了解来看,我觉得使用lea的版本会更慢,因为它增加了lea指令和mov指令之间的依赖性。
clanggcc都给出了我期望的结果,即4x mov

snz8szmq

snz8szmq1#

MSVC的代码比简单的mov方法要小。(但正如你所指出的,由于依赖性,它可能会潜在地变慢;你必须对此进行测试)。
mov ecx, 1是5个字节:一个字节用于操作码B8-BF,它也对寄存器进行编码,4个字节用于32位立即数。特别是,与一些算术指令不同,mov没有使用零或符号扩展来编码具有较少字节的较小立即数的选项。
lea ecx, [rdx-1]是3个字节。一个字节用于操作码;一个MOD R/M字节,其对存储器操作数的有效地址的目的寄存器ecx和基址寄存器rdx进行编码;并且(这里是密钥)* 一 * 字节用于8位符号扩展位移。
使用r8,r9的指令需要一个额外的字节作为雷克斯前缀;但这对movlea都是正确的,所以这是一个洗。

2eafrhcq

2eafrhcq2#

lea r32, [reg+disp8]是3个字节,而mov r32, imm32为5字节。
请参阅x86/x64机器代码中的高尔夫技巧和Nate的答案。
x86缺少mov reg, sign_extended_imm8。在所有其他条件相同(或几乎相同)的情况下,较小的代码大小通常更好,尤其是在可能必须来自传统解码的“冷”代码中。(也出于I高速缓存/ iTLB占用空间的原因。)
酷,我没有意识到任何编译器都在使用这种代码大小优化来实现寄存器中的常量。干得好,MSVC。GCC和Clang也应该这样做,至少在-Os**上是这样。甚至可能是-O2/-O3;会有一些情况下,这不是一个胜利,但我希望它的平均好在大多数CPU。
GCC/clang -Oz使用push imm8/pop reg进行代码大小优化,即使在性能上有很大的损失; Godbolt,也是3字节,但效率低得多。
英特尔自冰湖以来有4个/时钟lea(具有简单的寻址模式),而Zen一直都有。之前Skylake和更早版本上的2/时钟莱亚吞吐量,但仍然只有1个周期延迟。(https://uops.info/
我觉得使用lea的版本会更慢,因为它在莱亚指令和mov指令之间增加了依赖性。
这3个都从RDX读取mov-立即结果,因此存在良好的指令级并行性,而不是依赖链。RDX启动了一个新的依赖链,因此它可以在前端发出它之后的周期中尽早执行。
jmp之后读取结果的指令在流水线中时,如果在它们被调度到的执行单元上有任何空闲周期,则lea s可能已经执行。(或者,如果流水线中有很多独立的工作,而我们只是在后端ALU吞吐量上遇到瓶颈,那么tailcalled函数中的指令也不会在执行单元上得到一个周期。除非是加载而不是ALU,或者执行端口不忙碌……但是mov-imm也会有同样的问题,只是等待ALU执行端口的吞吐量,而不是延迟。)
uops are scheduled oldest-ready first,所以在正常情况下,前端远远领先于正在执行的最早的指令,像这样的独立工作通常可以找到一个间隙。
如果使用这些常量的任何指令将其与来自较旧指令的数据一起使用,则很可能物化常量的延迟将不是问题。我认为在R8/R9/RCX准备就绪之前的额外延迟不太可能在现代无序的exec x86中结束周期。
不过,有点奇怪的是,它把ECX的lea放在最后;许多函数首先查看它们的第一个参数,因此您希望它是mov-immediate或第一个lea。所有三个lea都可以并行执行,但最后一个可能会在一个周期后由前端发出。使用最旧的就绪优先调度,如果任何一个被调度到同一个端口(因为等待所有其他端口的uop数量很高),那么它们将发生资源冲突,不得不轮流使用。
我想知道编译器的算法是否会选择一个中间值,以使所有值更有可能在[reg+disp8]紧凑寻址模式的范围内。(希望它也更喜欢选择一个“遗留”寄存器,这样可以最小化雷克斯前缀;如果它选择了R8,则所有三个LEA都将需要雷克斯。)
如果执行端口压力相当均匀,那么在同一周期中发出时,它们可能 * 不会 * 都被调度到不同的端口。有关Haswell如何在同一周期中调度多个uop的详细信息,请参阅x86_64 haswell instruction scheduled on already used port instead of unused one。因此,这可能会产生资源冲突,使lea结果之一在mov结果就绪后2个周期才就绪。(2个周期,其中该端口是空闲的,如果ROB中有甚至更旧的uop,只是有一些间隙。)
所以这不是很确定,但我的直觉是,这在实践中不会成为问题。我猜(也希望)MSVC开发人员在一些现有的代码库上分析了它,没有发现任何严重的性能退化,希望能发现一些轻微的总体加速。

相关问题