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&lt;的情况下显然具有巨大的开销。 2.如果-Os gcc编译为:
mov rax, rcx
sub rax, rdx
cmp rax, 7
jg .L74
not DWORD PTR [rdx]
ret
.L74:
这让我觉得没有代码阻止编译器发出这个更短的代码。
编译器有没有理由这样做?有没有办法让他们编译到较短的版本而不编译大小?
这里的an example on Godbolt重现了这一点。它似乎与递归的复杂部分有关
答案 0 :(得分:2)
这是一个已知的编译器限制,请参阅我对该问题的评论。 IDK为什么存在;也许很难让编译器在没有完成保存regs的情况下决定他们可以做什么而不会溢出。
将早期检查拉入包装器通常很有用,因为它足够小,可以内联。
看起来现代gcc实际上有时可以回避这个编译器限制。
在Godbolt编译器资源管理器中使用你的例子,添加第二个调用者就足以让gcc6.1 -O2为你分割函数,因此它可以将早期内容插入第二个调用者并进入外部可见square()
(如果未采用早期退货路径,则以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()
本身编译为同一个东西,调用具有大量代码的私有克隆。来自克隆内部的递归调用调用包装函数,因此当不需要时,它们不会执行额外的推/弹工作。
即使在-O3时,即使没有其他来电者,gcc7也不会这样做。它仍然将其中一个递归调用转换为循环,但另一个只是再次调用大函数。
Clang 3.9和icc17也没有克隆函数,所以你应该手动编写可内联的包装器(如果在那里需要检查,则更改函数的主体以用于递归调用)。
您可能想要命名包装器square
,并将主体重命名为私有名称(例如static void square_impl
)。