zoukankan      html  css  js  c++  java
  • 浅谈C++ 异常处理的语义和性能

    异常处理是个十分深奥的主题,这里只是浅论其对C++性能的影响。

    在VC++中,有多个异常处理模式,三个最重要:

    • No exception handling (无异常处理)
    • C++ only (C++语言异常处理)
    • C++ 加SEH (C++语言加windows 结构异常处理机制)

    异常处理每增加一个级别,都要付出时空上的代价。我们从下面简单的C++例子着手,分析异常处理的原理及其性能:

    // simple class

    class MyAppObject

    {

        public:

           MyAppObject(int id) : _myID(id) {}

           ~MyAppObject();

           int _myID;

           void DoSomething(int throwWhat) ;

    }; 

    // can throw 2 different exception

    void MyAppObject::DoSomething(int throwWhat)

    {

        printf("MyAppObject::DoSomething called for '%d' ", _myID);

        switch (throwWhat)

        {

           case 0:

                 break;

           case 1:

                 this->_myID /= 0;             // exception 1

                 break;

           case 2:

                 throw SimpleString("error!"); // exception 2

                 break;

           }

    }

     // Test exception for the above class

    void TestMyAppObject()

    {

           printf("before try”); 

           try                                                          // line1

           {

                  printf("in try”);

                 MyAppObject so = 1;                          // line2

                 SimpleString ss("test ex point one");    // line3

                 so.DoSomething(1);                           // line4

                 printf("so::ID called for '%d' ", so._myID);

                 MyAppObject so2 = 2;                       // line5

                 printf("so2::ID called for '%d' ", so2._myID);

                 so2.DoSomething(0);                        // line6

           }

           catch(const SimpleString &e)                   // line7

           {

                 //printf("something happened: %s ", e);

           }

           catch(...)                                    //line8

           {

                 //printf("something happened: %s ", "SEH");

           }

    第一步,我们先选择“no exception”,并将上面line1,line7,line8注释掉。代码的size是:

    Exe

    Obj

    32,256 bytes

    20,931 bytes

    然而因为line4引入一个“除0”异常,我们的程序非正常地停止了工作。这并非什么大的灾难。但是如果这是关键的服务器程序,这样的结果肯定不能为客户接受。

    第二步,我们选择了,C++ only flag(/EHsc)。代码size变为:

    Exe

    Obj

    37,888 bytes

    24,959 bytes

    代码size较前面选择增加了近20%

    然而,这个选择决定了如果是C++的throw产生的异常我们可以俘获。操作系统产生的异常,比如windows SEH 异常机制产生的异常,也不能俘获。测试时,将line1line7Line8注释取消。

    运行程序,“0”异常仍然导致程序停止。然而,将line4输入改为2时,C++ throw 的异常被line7俘获。

    第三步,我们选择“C++ 加 SHE (/EHa)”,代码size变为:

    Exe

    Obj

    37.0 KB (37,888 bytes)

    28,486 bytes

    代码 obj size 略有变化,但是不显著。选择了这个后,MyAppObject::DoSomething的两种异常都能被俘获了。

    异常处理语义

    加了异常处理,程序的“工作集(working set)”, 的增长度高达20%,这是相当显著的。关键的软件部件必须考虑到这一点。那么,运行速度会不会受到影响呢?我们先看看异常处理的语义吧。

    上面的TestMyAppObject中,由于C++必须保证一旦异常出现,能“正确地”地销毁自动变量,比如TestMyAppObject中的soss,和 so2 变量。在有异常处理的情况下,必须区分“现行程序”的“区域”和“热点”。

    比如,TestMyAppObject区域before tryin try

    TestMyAppObject热点有line2 ~ line6 (每个line都是一个热点)。

    TestMyAppObject异常处理的逻辑是:

    • 做“stack unwinding (堆栈回滚)”:
      • 如果line2出异常,无须作什么(除非有MyAppObject 里有部分未完成构造的成员partially constructed member 问题)。
      • 如果line3出异常,so必须销毁。
      • 如果line4出异常,soss都必须销毁。
      • 如果line6出异常,sossso2都须销毁。
    • 如果找到catch,执行catch
    • 如果此函数没有catch,继续往上面函数,重复以上步骤  

    VC++的stack unwinding实现大致如此:

    异常处理逻辑可以转换成一个静态的jump列表(列出上面的四个热点的jump to 地址),和一个stack_unwind()函数(堆栈回滚函数),根据当前的”热点”,通过此列表,动态地跳到异常处里的回滚代码处。

    综合起来,异常处理在C++中,根据函数的auto变量的分布,必须在每个可能出现异常的函数添加上诉jump列表,导致程序size和工作集明显增加。但是测试表明,如果不出现异常,程序的执行速度的影响是可忽略的(仅仅需要保持热点位置),TestMyAppObject的测试结果选择异常处理(但不出异常)反而比选择不支持异常处理稍快。 

    出现异常后,TestMyAppObject的测试结果表明,程序速度的影响可以在10%~15%以上。但是我的测试还没有加rethrow 获者其它异常处理逻辑,仅仅俘获而已。

    另一个有趣的问题是,函数中auto变量的分布,对“热点列表”size的影响, 热点太多,会导致热点列表变得很大,所以如果可能,尽量把auto变量放在顶端:

    X a, b

    Y c,d

    而不是

    X a

    // do something (1

    X b;

    // do something else (2

    Y c;

    // do yet something else (3

    Y d;

    因为第一种分布只有一个热点(假设constructor 不会throw)。而第二种分布至少有三个热点。

    测试结果

    测试上述TestMyAppObject函数,循环1000次的结果: 

    • 传值0,使line4不出现异常(C++ throw),时间是0.802秒。

            传值1,使line4出现除零异常,时间是0.832秒。

    • 传值2,使line4出现异常(C++ throw),测试1000次测试,时间是1.043秒。

    这个结果我有下列观察:

    • C++ throw的代价明显高于windows SEH。C++ throw 异常在上述测试的时间比不出现异常增加近20%。但是如果我们throw简单的primitive 值,速度可能增快(读者可以自己测试)。
    • 除零异常在这里和无异常速度接近,但是考虑到本测试的简单性,和实际应用中try-catch可能纵跨多个函数,会线性增加stack-unwinding的代价。所以我认为,实际结果中,如果异常出现后出现性能10%~15%下降是正常的。
    • 另外要考虑的是OS和编译版本。VC++的异常处理比前面版本的性能大大提高了。 

    总结

    异常处理是C++中具有重要附加值的语言构造,为安全可靠的应用程序提供了基石。

    但是它也同时具有时空两方面的代价(trade off),我们在应用时要清楚这个方面。异常应该在“异常时”用 (好像是废话,其实是设计思想和模式的重要一环),不要把它当作方便的“控制构造 control construct”来用。如果应用容许,也要尽可能减少“热点”,减小热点列表。

  • 相关阅读:
    go——数组
    go——流程控制
    go——基本类型
    go——基本构成要素
    go——常量
    go——变量
    go——标准命令
    go——工程结构
    python 优雅的使用正则表达式 ~ 1
    python 安装操作 MySQL 数据库.
  • 原文地址:https://www.cnblogs.com/ly8838/p/3961119.html
Copyright © 2011-2022 走看看