带有早期 if 语句的函数中不必要的 pop 指令

Unneccessary pop instructions in functions with early if statement

在玩 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:

这让我相信没有代码阻止编译器发出这个较短的代码。

编译器这样做有什么原因吗?有没有办法让他们编译成较短的版本而不用编译大小?


an example on Godbolt 重现了这一点。好像和复杂的部分是递归的有关系

这是一个已知的编译器限制,请参阅我对该问题的评论。 IDK为什么存在;当编译器还没有完成保存 regs 时,可能很难决定他们可以做什么而不会溢出。

当包装小到足以内联时,将提前检查拉入包装器通常很有用。


看起来现代 gcc 有时实际上可以避开这个编译器限制。

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

code on 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 工作。


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


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

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