编译器是否将给定常量参数的简单函数简化为唯一指令?
Do compilers reduce simple functions given constant arguments into unique instructions?
我一直认为这是真的,但从未得到任何验证。考虑一个非常简单的函数:
int subtractFive(int num) {
return num -5;
}
如果对该函数的调用使用编译时常量,例如
getElement(5);
打开优化的编译器很可能会内联它。然而,我不清楚的是,如果 num - 5 将在运行时或编译时进行评估。表达式简化会以这种方式通过内联函数递归扩展吗?还是不超越功能?
我们可以简单的看一下生成的程序集就知道了。此代码:
int subtractFive(int num) {
return num -5;
}
int main(int argc, char *argv[]) {
return subtractFive(argc);
}
使用 g++ -O2
编译得到
leal -5(%rdi), %eax
ret
所以函数调用确实被简化为一条指令。这种优化技术被称为 inlining.
当然可以使用相同的技术来查看编译器将在多大程度上进行处理,例如稍微复杂一些
int subtractFive(int num) {
return num -5;
}
int foo(int i) {
return subtractFive(i) * 5;
}
int main(int argc, char *argv[]) {
return foo(argc);
}
仍然被编译为
leal -25(%rdi,%rdi,4), %eax
ret
所以这两个函数在编译时就被删除了。如果 foo
的输入在编译时已知,则函数调用(在这种情况下)将在编译时简单地替换为结果常量 (Live).
编译器还可以将此内联与常量折叠相结合,如果所有参数都是编译时常量,则用函数调用的完全计算结果替换函数调用。例如,
int subtractFive(int num) {
return num -5;
}
int foo(int i) {
return subtractFive(i) * 5;
}
int main() {
return foo(7);
}
编译为
mov eax, 10
ret
相当于
int main () {
return 10;
}
编译器总是会在它认为是个好主意的地方这样做,并且(通常)在优化这种低级别的代码方面比你做得更好。
聪明的编译器会在编译时评估它并替换 getElement(5),因为它永远不会有不同的结果。 None 个变量被认为是可变的。
做个小测试很容易;考虑以下
int foo(int);
int bar(int x) { return x-5; }
int baz() { return foo(bar(5)); }
用g++ -O3
编译函数baz
的asm输出是
xorl %edi, %edi
jmp _Z3fooi
这段代码在第一个参数中加载一个0,然后跳转到foo
的代码。所以来自 bar 的代码完全消失了,传递给 foo 的值的计算已经在编译时完成。
此外返回调用函数的值变成了跳转到函数代码(这称为"tail call optimization")。
我一直认为这是真的,但从未得到任何验证。考虑一个非常简单的函数:
int subtractFive(int num) {
return num -5;
}
如果对该函数的调用使用编译时常量,例如
getElement(5);
打开优化的编译器很可能会内联它。然而,我不清楚的是,如果 num - 5 将在运行时或编译时进行评估。表达式简化会以这种方式通过内联函数递归扩展吗?还是不超越功能?
我们可以简单的看一下生成的程序集就知道了。此代码:
int subtractFive(int num) {
return num -5;
}
int main(int argc, char *argv[]) {
return subtractFive(argc);
}
使用 g++ -O2
编译得到
leal -5(%rdi), %eax
ret
所以函数调用确实被简化为一条指令。这种优化技术被称为 inlining.
当然可以使用相同的技术来查看编译器将在多大程度上进行处理,例如稍微复杂一些
int subtractFive(int num) {
return num -5;
}
int foo(int i) {
return subtractFive(i) * 5;
}
int main(int argc, char *argv[]) {
return foo(argc);
}
仍然被编译为
leal -25(%rdi,%rdi,4), %eax
ret
所以这两个函数在编译时就被删除了。如果 foo
的输入在编译时已知,则函数调用(在这种情况下)将在编译时简单地替换为结果常量 (Live).
编译器还可以将此内联与常量折叠相结合,如果所有参数都是编译时常量,则用函数调用的完全计算结果替换函数调用。例如,
int subtractFive(int num) {
return num -5;
}
int foo(int i) {
return subtractFive(i) * 5;
}
int main() {
return foo(7);
}
编译为
mov eax, 10
ret
相当于
int main () {
return 10;
}
编译器总是会在它认为是个好主意的地方这样做,并且(通常)在优化这种低级别的代码方面比你做得更好。
聪明的编译器会在编译时评估它并替换 getElement(5),因为它永远不会有不同的结果。 None 个变量被认为是可变的。
做个小测试很容易;考虑以下
int foo(int);
int bar(int x) { return x-5; }
int baz() { return foo(bar(5)); }
用g++ -O3
编译函数baz
的asm输出是
xorl %edi, %edi
jmp _Z3fooi
这段代码在第一个参数中加载一个0,然后跳转到foo
的代码。所以来自 bar 的代码完全消失了,传递给 foo 的值的计算已经在编译时完成。
此外返回调用函数的值变成了跳转到函数代码(这称为"tail call optimization")。