举个例子:
#include <stdio.h>
int fun(int a) {
int res;
try
{
res = a *11;
if(res == 33)
throw 20;
}
catch (int e)
{
char *msg = "error";
printf(msg);
}
return res;
}
int main(int argc, char** argv) {
return fun(argc);
}
-O3
从编译器的角度来看,作为输入传递的数据是不可预见的,因此即使进行了优化以完全消除调用或异常系统,也无法做出任何假设。
在 LLVM IR 中,该fun
函数大致翻译为
define i32 @_Z3funi(i32 %a) #0 {
entry:
%mul = mul nsw i32 %a, 11 // The actual processing
%cmp = icmp eq i32 %mul, 33
br i1 %cmp, label %if.then, label %try.cont // jump if res == 33 to if.then
if.then: // lots of stuff happen here..
%exception = tail call i8* @__cxa_allocate_exception(i64 4) #3
%0 = bitcast i8* %exception to i32*
store i32 20, i32* %0, align 4, !tbaa !1
invoke void @__cxa_throw(i8* %exception, i8* bitcast (i8** @_ZTIi to i8*), i8* null) #4
to label %unreachable unwind label %lpad
lpad:
%1 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catch i8* bitcast (i8** @_ZTIi to i8*)
... // also here..
invoke.cont:
... // and here
br label %try.cont
try.cont: // This is where the normal flow should go
ret i32 %mul
eh.resume:
resume { i8*, i32 } %1
unreachable:
unreachable
}
如您所见,代码路径即使在正常控制流(无例外)的情况下很简单,现在也由同一功能中的几个基本块分支组成。
确实,在运行时几乎没有成本相关,因为您为使用的东西付费(如果您不扔,不会发生任何额外的事情),但是拥有多个分支也可能会损害您的性能,例如
- 分支预测变得更难
- 套准压力可能会大幅增加
- [其他]
当然,您不能在正常控制流和着陆点/异常入口点之间运行直通分支优化。
异常是一种复杂的机制,noexcept
即使在零成本 EH 的情况下,也极大地促进了编译器的生命。
编辑:在说明符的特定情况下noexcept
,如果编译器无法“证明”您的代码没有抛出,std::terminate
则会设置 EH(具有与实现相关的详细信息)。在这两种情况下(代码不会抛出和/或无法证明代码不会抛出),所涉及的机制更简单,编译器的约束更少。无论如何,出于优化原因,您并没有真正使用noexcept
它,它也是一个重要的语义指示。