当前位置: 首页 > 知识库问答 >
问题:

noexcept、堆栈展开和性能

卜鹏
2023-03-14

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

展开调用堆栈和可能展开调用堆栈之间的区别对代码生成有惊人的巨大影响。在no

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

我的问题是——当Scott Meyers谈到展开和可能展开时,他在谈论什么优化?为什么这些优化不适用于,扔()?他的评论只适用于2006年TR中提到的“代码”方法吗?

共有3个答案

黄隐水
2023-03-14

以以下示例为例:

#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中,有趣的功能大致翻译为:

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
}

如您所见,代码页,即使在正常控制流的情况下很简单(没有例外),现在也由同一个函数中的几个基本块分支组成。

的确,在运行时几乎没有成本,因为您需要为使用的内容付费(如果您不投掷,则不会发生任何额外的事情),但是拥有多个分支也可能损害您的性能,例如

    < li >分支预测变得更加困难 < li >套准压力可能会大幅增加 < Li >[其他]

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

异常是一种复杂的机制,即使在零成本 EH 中,noexcept 也极大地促进了编译器的生命周期

编辑:在< code>noexcept说明符的特定情况下,如果编译器无法“证明”您的代码没有抛出,则会设置一个< code>std::terminate EH(带有依赖于实现的详细信息)。在这两种情况下(代码不抛出和/或不能证明代码不抛出),涉及的机制更简单,编译器受到的约束更少。无论如何,你不会因为优化的原因而使用< code>noexcept,它也是一个重要的语义指示。

阎作人
2023-03-14

no的区别扔()的区别是,在扔()的情况下,异常堆栈仍然被解开,析构函数被调用,因此实现必须跟踪堆栈(参见15.5.2标准中的std::意外()函数)。

相反,std::terminate()不要求堆栈展开(15.5.1声明,在调用td::termnate()之前,堆栈是否展开是由实现定义的)。

GCC 似乎真的没有解开堆栈,除了: 演示
虽然叮当仍然展开:演示

(您可以在演示中注释f_noexcept()并取消注释f_emptythrow(),以查看GCC和clang都是如何展开堆栈的)

濮君植
2023-03-14

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

    < li >它生成执行特定操作的程序。 < li >它生成满足特定约束的程序。

TR说在表驱动的方法中没有开销,因为只要不发生抛出,就不需要采取任何行动。非异常执行路径是直接向前的。

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

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

表和单独的异常代码路径形式的空间开销(也称为膨胀)可能不会影响执行时间,但仍会影响下载程序并将其加载到RAM所需的时间。

这都是相对的,但noexcept会减少编译器的一些空闲时间。

 类似资料:
  • 作为一个系统语言,Rust 在底层运作。如果你有一个高级语言的背景,这可能有一些你不太熟悉的系统编程方面的内容。最重要的一个是内存如何工作,通过栈和堆。如果你熟悉类 C 语言是如何使用栈分配的,这个章节将是一个复习。如果你不太了解,你将会学到这个更通用的概念,不过是专注于 Rust 的。 对于大部分内容,当学习他们的时候,我们会使用一个简化的模型来开始。这使得你可以掌握基础,而不会现在就因为不相关

  • 我一直在使用java(jdk 6 hot spot JVM)进行垃圾收集。我希望社区能帮助我解决一些问题。 我的理解是: 1) 堆被分为 a)年轻一代-Eden和幸存者:新的对象和阵列被创建到年轻一代。次要的垃圾回收机制将在年轻一代中运行。对象,仍然活着,将从伊甸园空间移动到幸存者空间。 b)老一代/终身一代:主要收集将仍然活着的对象从年轻一代转移到老一代。 2)非堆分为 我想知道的是:

  • 问题内容: 内核堆栈和用户堆栈有什么区别?为什么要使用内核堆栈?如果在ISR中声明了局部变量,它将存储在哪里?每个进程都有自己的内核堆栈吗?那么,进程如何在这两个堆栈之间进行协调? 问题答案: 内核堆栈和用户堆栈有什么区别? 简而言之,除了在内存中使用不同的位置(并因此为堆栈指针寄存器使用不同的值)之外,什么也没有,而且通常使用不同的内存访问保护。也就是说,在用户模式下执行时,即使映射了内核内存(

  • 数字键盘字母组合问题[M]

  • 让我们回到函数,进行更深入的研究。 我们的第一个主题是 递归(recursion)。 如果你不是刚接触编程,那么你可能已经很熟悉它了,那么你可以跳过这一章。 递归是一种编程模式,在一个任务可以自然地拆分成多个相同类型但更简单的任务的情况下非常有用。或者,在一个任务可以简化为一个简单的行为加上该任务的一个更简单的变体的时候可以使用。或者,就像我们很快会看到的那样,处理某些数据结构。 当一个函数解决一

  • 我想了解SWIFT中的堆栈和堆中存储了什么。我有一个粗略的估计:你打印的所有东西和内存地址都不是值,那些存储在堆栈中,而打印出来的是值,那些在堆中,基本上是根据值和引用类型。我完全错了吗?另外,可以提供堆栈/堆的可视化表示吗?