C++异常处理优化
先说个实际遇到的问题。我们有个数据处理模块,里面有个函数每天要被调用几百万次。最初写的时候为了省事,直接在里面扔了try-catch块。看起来没问题是吧?但用VTune一分析,发现即使没有抛出异常,这个函数的异常处理开销也占了总执行时间的15%左右。这就很让人头疼了。
仔细研究了下发现,即使异常没有被抛出,编译器为了支持栈回退,也得生成额外的代码来记录每个对象的析构顺序。这些代码就是所谓的栈展开表。在函数执行时,这些信息虽然不会直接运行,但会影响代码的局部性和缓存命中率。
后来我们做了个简单的改动,把那些不太可能抛出异常的函数都加上了noexcept。就这一个改动,性能直接提升了8%。原因很简单,编译器看到noexcept就知道这个函数不需要生成复杂的栈展开信息,生成的代码更紧凑,执行效率自然就上去了。
不过要提醒的是,noexecpt不能乱用。如果你明明可能抛出异常却标记为noexcept,程序会直接terminate。我们团队就有人因为这个踩过坑,半夜被叫起来处理线上崩溃。
再说说异常抛出的优化。很多新手喜欢这样写:
但更好的做法是:
为什么?因为exception的构造函数不接受字符串参数,而runtime_error可以。前者需要先构造exception对象再设置what字符串,多了一次拷贝。
异常类型的设计也很讲究。我们项目里原来有个自定义异常,继承层次特别深,结果抛异常的时候构造开销特别大。后来简化了继承体系,性能明显改善。经验之谈:异常类最好直接继承std::exception,层次不要太深。
还有个技巧是使用异常指针。在某些需要跨线程传递异常的场景下,直接抛异常不行,可以用std::current_exception()和std::rethrow_exception()来传递,避免不必要的拷贝。
内存分配也是异常处理的一个坑。抛出异常时,编译器需要分配内存来存储异常对象。如果是在内存紧张的环境下,可能会抛出std::bad_alloc。我们曾经在嵌入式环境下遇到这个问题,最后是通过预分配异常对象池来解决的。
对于真正对性能要求极高的场景,可以考虑使用错误码替代异常。但这不是说异常就一无是处。错误码需要每次调用后都检查,而异常只在错误发生时处理。根据我们的测试,在错误发生率低于1%的场景下,异常的性能通常优于错误码。
最后分享一个实战案例:我们重构了一个图像处理模块中的关键函数,把里面的异常处理从函数内部移到了调用处,同时对不会抛出异常的子函数标记noexcept。这个改动让整个模块的吞吐量提升了12%。关键代码其实很简单:
优化前:
优化后:
当然,异常处理优化不是银弹,需要结合具体场景。我们的经验是:先用性能分析工具找到热点,再针对性地优化,别过早优化。毕竟代码的可维护性也很重要,不能为了性能把代码搞得一团糟。
总之,C++异常处理是个双刃剑,用好了能提升性能和代码质量,用不好就是性能杀手。关键是理解背后的机制,根据实际情况做出合适的选择。
更多推荐
所有评论(0)