以下来自Scott Meyers的新版C ++ 11书中的draft(第2页,第7-21行)
展开调用堆栈和可能展开调用堆栈之间的区别有一个 对代码生成产生了惊人的巨大影响。在noexcept函数中,优化器 如果出现异常,则无需将运行时堆栈保持在不可解除的状态 传播出函数,也不必确保noexcept中的对象 函数以相反的顺序销毁,应该是一个例外 离开这个功能。结果不仅仅是优化的更多机会 在noexcept函数的主体内,但也在函数所在的位置 调用。这种灵活性仅适用于noexcept功能。功能用 “throw()”异常规范缺少它,完全没有异常规范的函数也是如此。
相比之下,"Technical Report on C++ Performance"的5.4
部分描述了实现异常处理的“代码”和“表”方式。特别是,当没有抛出异常并且只有空间开销时,“table”方法显示没有时间开销。
我的问题是这个 - 斯科特迈尔斯在谈论解散和可能解散的时候谈到了什么优化?为什么这些优化不适用于throw()
?他的评论是否仅适用于2006 TR中提到的“代码”方法?
答案 0 :(得分:27)
有“无”开销,然后没有开销。您可以通过不同的方式考虑编译器:
TR表示在桌面驱动的appraoch中没有开销,因为只要不发生抛出就不需要采取任何操作。非特殊的执行路径直接进行。
但是,为了使表工作,非特殊代码仍然需要额外的约束。在任何异常可能导致其被破坏之前,需要对每个对象进行完全初始化,从而限制对可能抛出的调用的指令(例如,来自内联构造函数)的重新排序。同样,在任何可能的后续异常之前,必须完全销毁对象。
基于表的展开仅适用于遵循ABI调用约定的函数,具有堆栈帧。如果没有异常的可能性,编译器可以自由地忽略ABI并省略该帧。
空间开销,a.k.a.b胀,以表格形式和单独的特殊代码路径,可能不会影响执行时间,但它仍然会影响下载程序并将其加载到RAM中所花费的时间。
这都是相对的,但是noexcept
会使编译器有些松懈。
答案 1 :(得分:13)
noexcept
和throw()
之间的区别在于,在throw()
的情况下,异常堆栈仍然被解除并且析构函数被调用,因此实现必须跟踪堆栈(请参阅{标准中的{1}}。
相反,15.5.2 The std::unexpected() function
不需要展开堆栈(std::terminate()
声明它是实现定义的,无论堆栈是否在15.5.1
之前展开称为)。
GCC似乎真的没有为std::terminate()
:Demo清除堆栈
虽然clang仍在解开:Demo
(您可以在演示中发表评论noexcept
并取消注释f_noexcept()
,看看f_emptythrow()
GCC和clang都展开了筹码)
答案 2 :(得分:10)
采用以下示例:
#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
}
你可以看到代码路径,即使在正常控制流(没有例外)的情况下直截了当,现在由同一函数中的几个基本块分支组成。
确实,在运行时几乎没有成本关联,因为你支付你使用的东西(如果你不扔,没有额外的事情发生),但有多个分支也可能会损害您的表现,例如
当然,您无法在正常控制流和着陆点/异常入口点之间运行直通分支优化。
异常是一种复杂的机制,即使在零成本EH中,noexcept
也极大地促进了编译器的生命。
编辑:在noexcept
说明符的特定情况下,如果编译器不能“证明”您的代码没有抛出,std::terminate
EH是设置(具有依赖于实现的细节)。在这两种情况下(代码都不抛出和/或无法证明代码没有抛出),所涉及的机制更简单,编译器受限制更少。无论如何,出于优化原因,你并没有真正使用noexcept
,它也是一个重要的语义指示。
答案 3 :(得分:1)
我刚刚做了一个基准测试,以衡量为各种测试用例添加“ noexcept”说明符的性能影响:https://github.com/N-Dekker/noexcept_benchmark它有一个特定的测试用例,可以利用跳过堆栈展开的可能性,带有“ noexcept”:
void recursive_func(recursion_data& data) noexcept // or no 'noexcept'!
{
if (--data.number_of_func_calls_to_do > 0)
{
noexcept_benchmark::throw_exception_if(data.volatile_false);
object_class stack_object(data.object_counter);
recursive_func(data);
}
}
https://github.com/N-Dekker/noexcept_benchmark/blob/v03/lib/stack_unwinding_test.cpp#L48
从基准测试结果看,在此特定测试用例中,VS2017 x64和GCC 5.4.0似乎都通过添加'noexcept'产生了显着的性能提升。