繁体   English   中英

noexcept,堆栈展开和性能

[英]noexcept, stack unwinding and performance

以下来自Scott Meyers的新C ++ 11书中的草稿 (第2页,第7-21行)

展开调用堆栈和可能展开调用堆栈之间的区别对代码生成产生了惊人的巨大影响。 在noexcept函数中,如果异常将从函数传播出来,优化器不需要将运行时堆栈保持在不可解除的状态,也不必确保如果异常离开函数,则以构造的逆序销毁noexcept函数中的对象。 。 结果是更多的优化机会,不仅在noexcept函数的主体内,而且在调用函数的站点。 这种灵活性仅适用于noexcept功能。 具有“throw()”异常规范的函数缺少它,没有异常规范的函数也没有。

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

我的问题是这个 - 斯科特迈尔斯在谈论解散和可能解散的时候谈到了什么优化? 为什么这些优化不适用于throw() 他的评论是否仅适用于2006 TR中提到的“代码”方法?

有“没有”开销,然后没有开销。 您可以通过不同方式考虑编译器:

  • 它生成一个执行某些操作的程序。
  • 它生成满足某些约束的程序。

TR表示在表驱动的appraoch中没有开销,因为只要不发生抛出就不需要采取任何操作。 非特殊的执行路径直接进行。

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

基于表的展开仅适用于遵循ABI调用约定的函数,具有堆栈帧。 如果没有异常的可能性,编译器可以自由地忽略ABI并省略该帧。

以表格和单独的特殊代码路径形式的空间开销(又称膨胀)可能不会影响执行时间,但它仍然会影响下载程序并将其加载到RAM中所花费的时间。

这都是相对的,但noexcept削减了编译器的一些松懈。

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

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

GCC似乎真的没有为noexcept展开堆栈: Demo
虽然clang仍然解开: Demo

(您可以在演示中注释f_noexcept()并取消注释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);
}

从编译器的角度来看,作为输入传递的数据是不可预见的,因此即使使用-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 ,它也是一个重要的语义指示。

我只是做了一个基准测试来测量添加'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”获得显着的性能提升。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM