noexcept,堆栈展开和性能

斯科特·迈耶斯(Scott Meyers)的新C ++ 11书以下草稿 (第2页第7-21行)

调用堆栈展开和可能展开的区别对代码生成有着惊人的巨大影响。 在一个noexcept函数中,如果一个exception传播出函数,优化器不需要保持运行时堆栈处于不可展开状态,也不必确保noexcept函数中的对象按照构造的相反顺序销毁, 。 结果是更多的优化机会,不仅在noexcept函数体内,而且在函数被调用的地方。 这种灵活性只适用于noexceptfunction。 带有“throw()”exception规范的函数缺less它,就像没有exception规范的函数一样。

相比之下, “C ++性能技术报告”第5.4节则描述了实现exception处理的“代码”和“表格”方式。 特别是,当没有任何exception抛出时,“table”方法显示没有时间开销,只有空间开销。

我的问题是这样的 – 斯科特·迈耶斯(Scott Meyers)谈论的是什么样的优化? 为什么这些优化不适用于throw() ? 他的意见只适用于2006 TR中提到的“代码”方法吗?

没有“开销”,然后没有开销。 你可以用不同的方式来思考编译器:

  • 它生成一个执行某些操作的程序。
  • 它生成一个满足一定限制的程序。

TR表示,桌子上没有任何开销,因为只要不发生投掷,就不需要采取行动。 非例外的执行path直截了当。

但是,为了使表格工作,非例外的代码仍然需要额外的限制。 每个对象都需要在任何exception导致其破坏之前完全初始化,从而限制指令(例如从内联构造函数)重新sorting到可能的抛出调用。 同样,在任何可能的后续exception之前,对象必须被完全销毁。

基于表格的展开仅适用于遵循ABI调用约定的函数以及堆栈框架。 没有exception的可能性,编译器可能已经可以自由地忽略ABI并且省略了框架。

空间开销,也就是表格和单独的特殊代码path的forms,可能不会影响执行时间,但它仍然可以影响下载程序和将其加载到RAM中所花费的时间。

这一切都是相对的,但不noexcept编译器有些松懈。

noexceptthrow()之间的区别在于,在throw()的情况下,exception堆栈仍然被解开并且析构函数被调用,所以实现必须跟踪堆栈(参见15.5.2 The std::unexpected() function标准)。

相反, std::terminate()并不需要解开堆栈( 15.5.1声明它是实现定义的,不pipe堆栈在调用std::terminate()之前是否解开 )。

GCC似乎真的不解开堆栈noexcept : 演示
虽然铿锵仍然放松: 演示

(你可以在演示中对f_emptythrow()进行注释并取消注释f_emptythrow() ,以查看对于throw() ,GCC和clang都展开堆栈)

以下面的例子:

 #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); } 

从编译器的angular度来看,作为input传递的数据是不可预见的,因此即使-O3优化完全消除了呼叫或exception系统,也不能作出任何假设。

在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 } 

因为你可以看到代码path,即使在正常的控制stream(没有例外)的情况下是直接的,现在也由几个在同一个函数中的基本块分支组成。

确实,在运行时几乎没有成本是相关的,因为你支付你使用的东西 (如果你不扔,没有额外的事情发生),但有多个分支也可能会伤害你的performance,例如

  • 分支预测变得更加困难
  • 登记压力可能会大幅增加
  • [其他]

当然,你不能在正常控制stream程和着陆点/exception入口点之间运行直通分支优化。

exception是一个复杂的机制,即使在零成本的EH中,也noexcept大大地延长编译器的寿命。


编辑:在noexcept说明符的特定情况下,如果编译器不能' certificate '你的代码不抛出,build立一个std::terminate EH(与实现相关的细节)。 在这两种情况下(代码不会抛出和/或不能certificate代码不会抛出),涉及的机制更简单,编译器的约束也更less。 无论如何, noexcept优化的原因,你不会真的使用,这也是一个重要的语义指示。