函数中不必要的弹出指令,前面有if语句

Unneccessary pop instructions in functions with early if statement

本文关键字:前面 if 语句 指令 不必要 函数      更新时间:2023-10-16

在摆弄godbolt.org时,我注意到gcc(6.2, 7.0快照),clang(3.9)和icc(17)在编译接近

的东西时
int a(int* a, int* b) {
  if (b - a < 2) return *a = ~*a;
  // register intensive code here e.g. sorting network
}

将(-O2/-O3)编译成如下内容:

    push    r15
    mov     rax, rcx
    push    r14
    sub     rax, rdx
    push    r13
    push    r12
    push    rbp
    push    rbx
    sub     rsp, 184
    mov     QWORD PTR [rsp], rdx
    cmp     rax, 7
    jg      .L95
    not     DWORD PTR [rdx]
 .L162:
    add     rsp, 184
    pop     rbx
    pop     rbp
    pop     r12
    pop     r13
    pop     r14
    pop     r15
    ret

在b - a <2. 在-Os的情况下,gcc编译为:

    mov     rax, rcx
    sub     rax, rdx
    cmp     rax, 7
    jg      .L74
    not     DWORD PTR [rdx]
    ret
.L74:

这使我相信没有任何代码阻止编译器发出这段较短的代码。

编译器这样做是有原因的吗?有没有一种方法可以让它们编译成更短的版本而不需要编译大小?


这里有一个关于Godbolt的例子,它再现了这个。这似乎与复杂部分递归有关

这是一个已知的编译器限制,请参阅我对该问题的评论。IDK为什么存在;也许对于编译器来说,当它们还没有完成保存regs时,很难决定它们可以在不溢出的情况下做些什么。

将提前取出的签入包装器中,当它足够小而可以内联时,通常是有用的。


看起来现代的gcc有时可以绕过这个编译器的限制。

在Godbolt编译器资源管理器上使用您的示例,添加第二个调用者足以让gcc6.1 -O2为您拆分函数,因此它可以将早期输出内联到第二个调用者和外部可见的square()(如果不采用早期输出返回路径,则以jmp square(int*, int*) [clone .part.3]结束)。

在Godbolt上的

代码,注意我添加了-std=gnu++14,这是clang编译你的代码所必需的。

void square_inlinewrapper(int* a, int* b) {
  //if (b - a < 16) return;  // gcc inlines this part for us, and calls a private clone of the function!
  return square(a, b);
}
# gcc6.1 -O2  (default / generic -march= and -mtune=)
    mov     rax, rsi
    sub     rax, rdi
    cmp     rax, 63
    jg      .L9
    rep ret
.L9:
    jmp     square(int*, int*) [clone .part.3]

square()本身编译为相同的东西,调用具有大量代码的私有克隆。来自克隆内部的递归调用调用包装器函数,因此它们不会在不需要时执行额外的push/pop工作。


即使gcc7在没有其他调用者时也不会这样做,即使是在-O3。它仍然将其中一个递归调用转换为循环,但另一个只是再次调用大函数。


Clang 3.9和icc17也不克隆函数,因此您应该手动编写可内联的包装器(并更改函数的主体以将其用于递归调用,如果那里需要检查的话)。

您可能希望将包装器命名为square,并将主体重命名为私有名称(如static void square_impl)。