Phi*_*lip 96 c c# c++ java compiler-optimization
这是我作为一个经验不足的程序员经常遇到的情况,我特别想知道我正在努力优化的一个雄心勃勃,速度密集的项目.对于主要的类C语言(C,objC,C++,Java,C#等)及其常用的编译器,这两个函数是否同样有效运行?编译代码有什么区别吗?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Run Code Online (Sandbox Code Playgroud)
基本上,是否有提前break
或return
早期的直接效率奖金/罚款?堆栈框架是如何涉及的?是否有优化的特殊情况?是否有任何因素(如内联或"Do stuff"的大小)可能会对此产生重大影响?
我总是支持改进可读性而非次要优化(我通过参数验证看到foo1很多),但这种情况经常发生,我想一劳永逸地抛开所有的担忧.
而且我知道过早优化的陷阱......呃,这些都是一些痛苦的回忆.
编辑:我接受了答案,但EJP的答案非常简洁地解释了为什么使用a return
几乎可以忽略不计(在汇编中,return
在函数末尾创建一个'分支',这非常快.分支改变了PC寄存器和也可能会影响缓存和管道,这是非常小的.)特别是对于这种情况,它实际上没有区别,因为它们if/else
和return
函数末尾都创建了相同的分支.
Dan*_*ani 92
根本没有区别:
=====> cat test_return.cpp
extern void something();
extern void something2();
void test(bool b)
{
if(b)
{
something();
}
else
something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();
void test(bool b)
{
if(b)
{
something();
return;
}
something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp
=====> g++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp
=====> clang++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====>
Run Code Online (Sandbox Code Playgroud)
即使在两个编译器中没有优化,生成的代码也没有区别
blu*_*ift 65
简短的回答是,没有区别.帮自己一个忙,不要再担心了.优化编译器几乎总是比你聪明.
专注于可读性和可维护性.
如果你想看看会发生什么,请在优化的基础上构建它们并查看汇编器输出.
cfi*_*cfi 28
有趣的答案:虽然我同意所有这些(到目前为止),但这个问题的可能内涵到目前为止完全被忽视了.
如果通过资源分配扩展上面的简单示例,然后使用可能导致资源释放的错误检查,则图片可能会更改.
考虑初学者可能采取的天真方法:
int func(..some parameters...) {
res_a a = allocate_resource_a();
if (!a) {
return 1;
}
res_b b = allocate_resource_b();
if (!b) {
free_resource_a(a);
return 2;
}
res_c c = allocate_resource_c();
if (!c) {
free_resource_b(b);
free_resource_a(a);
return 3;
}
do_work();
free_resource_c(c);
free_resource_b(b);
free_resource_a(a);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
以上将代表过早返回风格的极端版本.注意当代码的复杂性增加时,代码如何变得非常重复且不可维护.如今人们可能会使用异常处理来捕获这些.
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
try {
a = allocate_resource_a(); # throws ExceptionResA
b = allocate_resource_b(); # throws ExceptionResB
c = allocate_resource_c(); # throws ExceptionResC
do_work();
}
catch (ExceptionBase e) {
# Could use type of e here to distinguish and
# use different catch phrases here
# class ExceptionBase must be base class of ExceptionResA/B/C
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
throw e
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在看了下面的goto示例之后,Philip建议在上面的catch块中使用一个无断开关/ case.可以切换(typeof(e))然后通过free_resourcex()
调用,但这不是微不足道的,需要设计考虑.请记住,没有中断的开关/外壳与下面带有菊花链标签的goto完全相同......
正如Mark B所指出的那样,在C++中,遵循资源获取是初始化原则,简称RAII被认为是好的风格.该概念的要点是使用对象实例化来获取资源.一旦对象超出范围并调用其析构函数,资源就会自动释放.对于相互依存的资源,必须特别注意确保解除分配的正确顺序并设计对象类型,以便所有析构函数都可以获得所需的数据.
或者在例外日期之前可能会:
int func(..some parameters...) {
res_a a = allocate_resource_a();
res_b b = allocate_resource_b();
res_c c = allocate_resource_c();
if (a && b && c) {
do_work();
}
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
但是这个过度简化的示例有几个缺点:只有在分配的资源不相互依赖的情况下才能使用它(例如,它不能用于分配内存,然后打开文件句柄,然后从句柄读取数据到内存中),它不提供单独的,可区分的错误代码作为返回值.
为了保持代码快速(!),紧凑,易于阅读和可扩展,Linus Torvalds为处理资源的内核代码强制执行不同的风格,甚至以一种绝对有意义的方式使用臭名昭着的goto:
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
a = allocate_resource_a() || goto error_a;
b = allocate_resource_b() || goto error_b;
c = allocate_resource_c() || goto error_c;
do_work();
error_c:
free_resource_c(c);
error_b:
free_resource_b(b);
error_a:
free_resource_a(a);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
关于内核邮件列表的讨论要点是,与goto语句相比"首选"的大多数语言特性都是隐式的,例如巨大的,类似树的if/else,异常处理程序,循环/中断/继续语句等等.上面例子中的goto被认为是好的,因为它们只是跳跃一小段距离,有清晰的标签,并且可以释放其他杂乱的代码来跟踪错误情况.这个问题也在stackoverflow上讨论过.
但是,上一个示例中缺少的是返回错误代码的好方法.我想result_code++
在每次free_resource_x()
调用之后添加一个并返回该代码,但这会抵消上述编码风格的一些速度提升.如果成功,很难返回0.也许我只是缺乏想象力;-)
所以,是的,我确实认为编码过早回报的问题存在很大差异.但我也认为只有在更复杂的代码中才能显示出更难或不可能重构和优化编译器.一旦资源分配发挥作用,通常就是这种情况.
具体而言,return
将被编译到方法末尾的分支中,其中将存在RET
指令或其可能的任何指令.如果你把它留出来,那么块之前的块else
将被编译成一个分支到else
块的末尾.所以你可以在这个特定情况下看到它没有任何区别.