代码之家  ›  专栏  ›  技术社区  ›  Gilles-Philippe Paillé

使用LEA而不是MOV在汇编中传递参数的优势++

  •  1
  • Gilles-Philippe Paillé  · 技术社区  · 3 年前

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

    #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 指示

    clang gcc 两者都给出了我所期望的结果,即4x mov

    2 回复  |  直到 3 年前
        1
  •  8
  •   Sep Roland    3 年前

    MSVC的代码比天真的要小 mov 方法(但正如您所指出的,由于依赖性,它可能会更慢;您必须对此进行测试。)

         1                                          bits 64
         2 00000000 BA02000000                      mov     edx, 2
         3 00000005 448D4A02                        lea     r9d, QWORD [rdx+2]
         4 00000009 448D4201                        lea     r8d, QWORD [rdx+1]
         5 0000000D 8D4AFF                          lea     ecx, QWORD [rdx-1]
         6                                  
         7 00000010 B901000000                      mov     ecx, 1
         8 00000015 BA02000000                      mov     edx, 2
         9 0000001A 41B803000000                    mov     r8d, 3
        10 00000020 41B904000000                    mov     r9d, 4
    

    mov ecx, 1 是5个字节:一个字节用于操作码B8-BF,它也对寄存器进行编码,4个字节用于32位立即数。特别是,与某些算术指令不同 mov 使用零或符号扩展以较少的字节对较小的立即数进行编码。

    lea ecx, [rdx-1] 是3个字节。操作码的一个字节;一个MOD R/M字节,用于对目标寄存器进行编码 ecx 和基极寄存器 rdx 用于存储器操作数的有效地址;和(这是钥匙) 用于8位符号扩展位移的字节。

    使用说明 r8,r9 需要一个额外的字节作为REX前缀;但两者都是如此 mov lea 所以这是一次洗涤。

        2
  •  5
  •   Peter Cordes    3 年前

    lea r32, [reg+disp8] 是3字节,而不是。 mov r32, imm32 为5字节。
    看见 Tips for golfing in x86/x64 machine code 以及内特的回答。

    x86不幸缺少 mov reg, sign_extended_imm8 在其他条件相同(或几乎相同)的情况下,较小的代码大小通常更好,尤其是在可能必须来自传统解码的“冷”代码中。(也出于I-cache/iTLB占用空间的原因。)


    很酷,我没有意识到任何编译器都在使用这种代码大小优化来实现寄存器中的常量。 干得好,微软风投。GCC和Clang也应该这样做,至少与 -Os 。甚至可能 -O2 / -O3 ;在某些情况下,这不是一场胜利,但我预计它在大多数CPU上的平均表现都很好。

    GCC/clang -Oz 使用 push imm8 / pop reg 用于代码大小优化,即使在显著的性能成本下也是如此; Godbolt 。这也是3个字节,但效率要低得多。

    英特尔自冰湖有4/时钟 lea (使用简单的寻址模式),而Zen一直都是这样。以前Skylake和更早版本的2/时钟LEA吞吐量,但仍然只有1个周期延迟。( https://uops.info/ )


    我感觉使用的版本 lea 由于它在lea指令和 mov 指示

    所有3个读取 mov -RDX的直接结果,所以有很好的指令级并行性,而不是依赖关系链。RDX启动了一个新的依赖链,因此它最早可以在前端发布后的周期内执行。

    按时间指示 jmp 读取的结果在管道中 lea 如果在计划执行的执行单元上有任何空闲周期,则s可能已经执行。(或者,如果管道中有很多独立的工作,而我们只是在后端ALU吞吐量上遇到瓶颈,那么尾调用函数中的指令也不会在执行单元上得到循环。除非可能是加载而不是ALU,或者执行端口不忙 mov -imm也会遇到同样的问题,只是等待ALU执行端口吞吐量,而不是等待延迟。)

    ( uops are scheduled oldest-ready first ,因此,在正常情况下,前端远远领先于正在执行的最旧指令,像这样的独立工作通常会发现差距。)

    如果使用这些常量的任何指令将其与来自旧指令的数据一起使用,那么很可能实现常量的延迟将不是问题。 我认为在R8/R9/RCX准备就绪之前的额外延迟不太可能在现代无序的execx86中花费周期。

    它把 lea 不过,ECX是最后一个;许多函数首先查看它们的第一个arg,所以您希望它是 mov -立即或第一个 lea .三者 lea s可以并行执行,但最后一个可能会在一个周期后由前端发出。使用最旧的就绪优先调度,如果有任何一个被调度到同一个端口(因为等待所有其他端口的uop数量都很高),那么它们将发生资源冲突,必须轮流进行。

    我想知道编译器的算法是否选择了一个中间值,以使所有值都在 [reg+disp8] 紧凑寻址模式。(希望它也更喜欢选择一个“遗留”寄存器,这样REX前缀就可以最小化;如果它选择了R8,那么所有三个LEA都需要一个REX。)

    如果执行端口压力相当均匀,他们可能 当在同一周期中发出时,所有端口都被调度到不同的端口。看见 x86_64 haswell instruction scheduled on already used port instead of unused one 有关Haswell如何在同一周期中调度多个uop的详细信息。因此,这可能会造成资源冲突 lea mov 结果已经准备好了。(如果ROB中有更旧的uop只有一些间隙,则该端口空闲的2个周期。)

    所以这不是很明确,但我的直觉是,这在实践中不会成为问题。我想(也希望)MSVC开发人员在一些现有的代码库中对其进行了分析,没有发现任何严重的性能退化,并希望平均发现一些轻微的总体加速。

    推荐文章