关于gcc翻译的汇编代码用于C实现N阶乘的疑惑

问题描述

我对阶乘函数的反汇编感到困惑。

C 代码

long factorial(int x)
{
    long result = 1;

    while (x > 1)
    {
        result = result * x;
        x= x - 1;
    }

    return result;
}

我使用 gcc 命令反汇编阶乘函数 gcc -S -O1 test.c

factorial:
.LFB0:
        cmpl    $1,%edi
        jle     .L2
        movslq  %edi,%rdx
        leaq    -1(%rdx),%rcx
        leal    -2(%rdi),%eax
        subq    %rax,%rcx
        movl    $1,%eax
.L3:
        imulq   %rdx,%rax
        subq    $1,%rdx
        cmpq    %rcx,%rdx
        jne     .L3
.L2:
        movl    $1,%eax
        rep ret

我不明白下面的代码是干什么的,谁能帮帮我?

movq    %rax,%rdx
leaq    -1(%rax),%rcx
leal    -2(%rdi),%esi
subq    %rsi,%rcx

解决方法

(对问题的更新更改了 C 和 asm,删除了问题仍然询问的 movq %rax,%rdx,但否则会使答案的第一部分无效。请参阅编辑历史记录或遵循此中的 Godbolt 链接回答以查看本节所指的内容。)

movq %rax,%rdx 正在制作符号扩展 x(32 位 int 到 64 位 long)的副本,用于表达式中的循环result * x 表达式隐式执行 (long)x。请注意,它避免了每次循环都像 C 抽象机那样重做符号扩展。 (与 GCC5 及更早版本不同,后者或多或少按编写的方式编译,仅进行正常的转换 like do{}while loop structure。)

它以符号扩展 x 的 2 个副本开始的事实是因为您的 C 以 result=x 开头。这是您的阶乘实现中的一个错误,因为您没有执行 x--,但编译器只是在实现您编写的内容。实际上使用 x-- 会产生其他奇怪的代码 (https://godbolt.org/z/345K6hbas),例如 leal -3(%rdi),%edi / addq $1,%rdi,它仅与 lea -2(%rdi),%edi 不同,以防 LEA 产生 0xFFFFFFFF (-1) 和qword +1 进入高 32 位。但这不可能发生,因为较早的 cmp/jcc 会提前返回 x-1 <= 1,因此 rdi-3+1 是另一个错过的优化。


其他 3 条指令(lea/lea/sub)是 GCC 愚蠢的,我认为以复杂的方式计算常量 1 作为 RCX 中的循环终止条件,以进行比较反对RDX。这是一个遗漏的优化错误,您可以在 GCC's bugzilla 上报告,因为它仍然发生在 -O2 (https://godbolt.org/z/achGeePYb) 的当前主干夜间构建中。

我猜,提升符号扩展导致创建此逻辑为时已晚,优化传递无法将其归类为合理的东西,或者以他们不能/不可以的方式。


顺便说一句,这看起来像 GCC7,因为它与您的 asm https://godbolt.org/z/jMhjsvfdM 匹配。后来的 GCC 省略了 rep 前缀(但否则会造成同样的混乱),早期的 GCC 要么使 asm 略有不同,要么(gcc5 及更早版本)直接进入循环而没有先做那么多。但是他们在每次循环迭代(从 32 位 x 到 64 位 int)时都会重做 long 的符号扩展。

即使在 -O2 时也会发生这种情况,因此这不是仅启用部分优化 (-O1) 的结果。 GCC8 和更早版本在 -O3 处自动矢量化,但这可能无利可图,这可能是 GCC9 和后来停止这样做的原因。 (x86 在 AVX-512 之前没有 SIMD qword 乘法,-march=skylake-avx512,并且从多个 pmuludq 操作中合成它很慢。