57

这个问题对于嵌入式开发尤其重要。异常处理为生成的二进制输出增加了一些足迹。另一方面,无一例外地需要以其他方式处理错误,这需要额外的代码,最终也会增加二进制大小。

我对你的经历很感兴趣,尤其是:

  1. 您的编译器为异常处理添加的平均占用空间是多少(如果您有这样的测量结果)?
  2. 就二进制输出大小而言,异常处理真的比其他错误处理策略更昂贵(很多人这么说)吗?
  3. 对于嵌入式开发,您会建议什么错误处理策略?

请仅将我的问题作为指导。欢迎任何意见。

附录:是否有人有具体的方法/脚本/工具,对于特定的 C++ 对象/可执行文件,将显示编译器生成的专用于异常处理的代码和数据结构占用的已加载内存占用的百分比?

4

8 回答 8

37

当发生异常,会有时间开销,这取决于您如何实现异常处理。但是,作为轶事,应该导致异常的事件的严重性将花费同样多的时间来使用任何其他方法来处理。为什么不使用高度支持的基于语言的方法来处理此类问题?

GNU C++ 编译器默认使用零成本模型,即在不发生异常时没有时间开销。

由于有关异常处理代码和本地对象偏移量的信息可以在编译时计算一次,因此这些信息可以保存在与每个函数相关联的单个位置,而不是每个 ARI 中。您基本上消除了每个 ARI 的异常开销,从而避免了将它们推入堆栈的额外时间。这种方法称为异常处理的零成本模型,前面提到的优化存储称为影子堆栈。- Bruce Eckel,用 C++ 思考第 2 卷

大小复杂性开销不容易量化,但 Eckel 表示平均为 5% 和 15%。这将取决于您的异常处理代码的大小与应用程序代码的大小之比。如果您的程序很小,那么异常将是二进制文件的很大一部分。如果您使用的是零成本模型,那么异常将占用更多空间来消除时间开销,因此,如果您关心空间而不是时间,则不要使用零成本编译。

我的观点是大多数嵌入式系统都有足够的内存,如果你的系统有一个 C++ 编译器,你就有足够的空间来包含异常。我的项目使用的 PC/104 计算机有几 GB 的辅助内存,512 MB 的主内存,因此对于异常没有空间问题 - 但是,我们的微控制器是用 C 编程的。我的启发是“如果有一个主流的 C++ 编译器它,使用异常,否则使用 C"。

于 2009-03-27T20:13:18.227 回答
22

测量事物,第 2 部分。我现在有两个程序。第一个是 C 语言,用 gcc -O2 编译:

#include <stdio.h>
#include <time.h>

#define BIG 1000000

int f( int n ) {
    int r = 0, i = 0;
    for ( i = 0; i < 1000; i++ ) {
        r += i;
        if ( n == BIG - 1 ) {
            return -1;
        }
    }
    return r;
}

int main() { 
    clock_t start = clock();
    int i = 0, z = 0;
    for ( i = 0; i < BIG; i++ ) {
        if ( (z = f(i)) == -1 ) { 
            break;
        }
    }
    double t  = (double)(clock() - start) / CLOCKS_PER_SEC;
    printf( "%f\n", t );
    printf( "%d\n", z );
}

第二个是C++,有异常处理,用g++ -O2编译:

#include <stdio.h>
#include <time.h>

#define BIG 1000000

int f( int n ) {
    int r = 0, i = 0;
    for ( i = 0; i < 1000; i++ ) {
        r += i;
        if ( n == BIG - 1 ) {
            throw -1;
        }
    }
    return r;
}

int main() { 
    clock_t start = clock();
    int i = 0, z = 0;
    for ( i = 0; i < BIG; i++ ) {
        try {
         z += f(i); 
        }
        catch( ... ) {
            break;
        }

    }
    double t  = (double)(clock() - start) / CLOCKS_PER_SEC;
    printf( "%f\n", t );
    printf( "%d\n", z );
}

我认为这些回答了对我上一篇文章的所有批评。

结果:执行时间使 C 版本比 C++ 版本有 0.5% 的优势,但有例外,而不是其他人谈论的 10%(但未演示)

如果其他人可以尝试编译和运行代码(应该只需要几分钟)以检查我是否在任何地方都没有犯过可怕而明显的错误,我将非常感激。这就是所谓的“科学方法”!

于 2009-03-27T22:47:20.493 回答
6

我在低延迟环境中工作。(我的应用程序在生产“链”中的时间不到 300 微秒)根据我的经验,异常处理会增加 5-25% 的执行时间,具体取决于您执行的数量!

我们通常不关心二进制膨胀,但如果膨胀过多,就会像疯了一样颠簸,所以你需要小心。

保持二进制合理(取决于您的设置)。

我对我的系统进行了相当广泛的分析。
其他讨厌的地方:

日志记录

持久化(我们只是不这样做,或者如果我们这样做是并行的)

于 2009-03-27T19:56:00.123 回答
5

我想这取决于该特定平台的硬件和工具链端口。

我没有数字。然而,对于大多数嵌入式开发,我看到人们抛弃了两件事(对于 VxWorks/GCC 工具链):

  • 模板
  • RTTI

在大多数情况下,异常处理确实同时使用了这两种方法,因此也倾向于将其丢弃。

在那些我们真的想接近金属的情况下,使用setjmp/ longjmp请注意,这可能不是最好的解决方案(或非常强大),但这就是_we_使用的。

您可以使用带有/不带有异常处理的两个版本的基准测试套件在您的桌面上运行简单的测试,并获得您最依赖的数据。

关于嵌入式开发的另一件事:模板像瘟疫一样被避免——它们会导致过多的膨胀。Johann Gerell 在评论中解释了模板和 RTTI 的异常标记(我认为这很好理解)。

同样,这正是我们所做的。所有的反对票是怎么回事?

于 2009-03-27T19:29:23.340 回答
4

需要考虑的一件事:如果您在嵌入式环境中工作,您希望应用程序尽可能小。Microsoft C 运行时给程序增加了相当多的开销。通过删除 C 运行时作为要求,我能够得到一个简单的程序,它是一个 2KB 的 exe 文件,而不是一个 70 多千字节的文件,而且所有的大小优化都打开了。

C++ 异常处理需要由 C 运行时提供的编译器支持。细节笼罩在神秘之中,根本没有记录。通过避免 C++ 异常,我可以删除整个 C 运行时库。

您可能会争辩说只是动态链接,但在我的情况下这是不切实际的。

另一个问题是 C++ 异常至少在 MSVC 上需要有限的 RTTI(运行时类型信息),这意味着异常的类型名称存储在可执行文件中。在空间方面,这不是问题,但对我来说,文件中没有这些信息只是“感觉”更干净。

于 2009-04-01T08:43:47.097 回答
2

很容易看到对二进制大小的影响,只需在编译器中关闭 RTTI 和异常。如果您正在使用它,您会收到有关 dynamic_cast<> 的投诉……但我们通常会避免在我们的环境中使用依赖于 dynamic_cast<> 的代码。

我们一直发现,在二进制大小方面关闭异常处理和 RTTI 是一种胜利。在没有异常处理的情况下,我见过许多不同的错误处理方法。最受欢迎的似乎是将失败代码向上传递到调用堆栈。在我们当前的项目中,我们使用 setjmp/longjmp,但我建议不要在 C++ 项目中这样做,因为在许多实现中退出作用域时它们不会运行析构函数。老实说,我认为这是代码的原始架构师做出的一个糟糕的选择,特别是考虑到我们的项目是 C++。

于 2009-04-01T08:49:46.363 回答
1

在我看来,异常处理不是嵌入式开发通常可以接受的。

GCC 和 Microsoft 都没有“零开销”异常处理。两个编译器都将序言和结尾语句插入到跟踪执行范围的每个函数中。这导致性能和内存占用显着增加。

根据我的经验,性能差异大约为 10%,这对于我的工作领域(实时图形)来说是一个巨大的数字。内存开销要少得多,但仍然很重要——我不记得这个数字了,但是使用 GCC/MSVC 很容易以两种方式编译你的程序并测量差异。

我见过一些人将异常处理称为“仅当您使用它时”的成本。根据我的观察,这不是真的。当您启用异常处理时,它会影响所有代码,无论代码路径是否可以引发异常(当您考虑编译器的工作方式时,这完全有意义)。

我也会远离 RTTI 进行嵌入式开发,尽管我们确实在调试版本中使用它来检查向下转换的结果。

于 2009-03-27T19:46:25.733 回答
0

定义“嵌入式”。在 8 位处理器上,我肯定不会处理异常(我肯定不会在 8 位处理器上使用 C++)。如果您使用的 PC104 类型板的功能强大到足以在几年前成为某人的桌面,那么您可能会侥幸成功。但我不得不问——为什么会有例外?通常在嵌入式应用程序中,发生异常之类的事情是不可想象的——为什么在测试中没有解决这个问题?

例如,这是在医疗设备中吗?医疗设备中的草率软件已经杀死了人。任何计划外发生的事情都是不可接受的,期间。必须考虑所有故障模式,正如 Joel Spolsky 所说,异常就像 GOTO 语句,除了你不知道它们是从哪里调用的。所以当你处理你的异常时,什么失败了,你的设备处于什么状态?由于您的例外,您的放射治疗机卡在 FULL 状态并且正在煮活人(这发生在 IRL)?在您的 10,000 多行代码中,异常发生在什么时候。当然,您可以将其减少到大约 100 行代码,但您知道导致异常的每一行代码的重要性吗?

如果没有更多信息,我会说不要计划嵌入式系统中的异常。如果您添加它们,则准备好计划可能导致异常的每一行代码的故障模式。如果您正在制造医疗设备,那么如果您不这样做,人们就会死去。如果您正在制作便携式 DVD 播放器,那么您制作的便携式 DVD 播放器很糟糕。它是哪一个?

于 2009-04-02T15:30:18.243 回答