带有早期 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
)。
在玩 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
)。