C++异常的幕后附录I:异常的真正代价
原文地址:https://monoinfinito.wordpress.com/2013/06/11/c-exceptions-under-the-hood-appendix-i-the-true-cost-of-an-exception/
作者:nicolasbrailo
还记得很久以前,当关于异常处理的系列文章刚刚开始时,我提到的这些文章只适用于gcc/x86吗?原因是不是所有的编译器都以相同的方式实现异常。特别的,有两个主要实现方式:
- 使用一个查找表以及一些元数据,像Itanium ABI规定的;这是我们讨论的。
- Sj/Lj(ARM):在进出一个方法时注册异常处理信息。
Gcc(以及许多其他编译器)在x86上实现这个ABI的方式是使用元数据(.gcc_except_table与CFI)。虽然它相当难解析,在运行时抛出异常时可能需要长时间解析,它有很大的好处:如果没有抛出异常,则无需设置的代价。这称为“零代价异常处理”,因为在没有异常抛出时,正常的执行无需付出代价。性能就像我们指明nothrow那样。这是正确的,把代码局部性与缓存问题放在一旁,不管是否使用异常,都不会影响性能,除非实际抛出异常。这是极大的优势,并且它符合c++的哲学,即不使用的特性没有代价。
在这些文章使用的程序中,在声明一个方法时使用noexcept说明(或者空的throw说明符,C++11以前),编译器将省略.gcc_except_table的生成。这使得代码更紧凑,这将改善缓存的使用,但这不太可能对应用程序的性能产生显著的影响。
如果我们讨论ARM,Sj/Lj看起来是缺省的选项(我相信这是有原因的,但我没有足够的ARM经验知道这一点)。这个异常处理方法基于在进出一个方法时注册的异常处理信息,如果抛出异常,这个方法使用异常或要求清理。这将导致更快的异常处理,但不管是否抛出异常都压迫付出代价。
如果你对sjlj以及零代价异常处理感兴趣,LLVM有很棒的文档。
C++异常的幕后附录II:C++的元类与RTTI
原文地址:https://monoinfinito.wordpress.com/2013/06/13/c-exceptions-under-the-hood-appendix-ii-metaclasses-and-rtti-on-c/
作者:nicolasbrailo
很久以前,当我们正准备开始写我们无需libstdc++辅助处理异常的最小化ABI时,我们添加了一个空类来取悦链接器:
1 2 3 4 5 | namespace __cxxabiv1 { struct __class_type_info { virtual void foo() {} } ti; } |
我提到这个类用于检查一个catch是否可以处理抛出异常的一个子类,但这又意味着什么呢?让我们对抛出函数稍作修改,看我们开始处理继承时发生什么。你可能想看这些例子的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct Derived_Exception : public Exception {}; void raise() { throw Derived_Exception(); } void catchit() { try { raise(); } catch(Exception&) { printf("Caught an Exception!\n"); } catch(Derived_Exception&) { printf("Caught a Derived_Exception!\n"); } printf("catchit handled the exception\n"); } |
在这个例子里会发生什么是非常清楚的:它应该打印出“Catch an Exception”,因为catch块应该能够处理Exception与Derived_Exception。不仅如此,如果我们编译throw.cpp,我们将得到应该警告,让我们知道第二个catch是死代码:
1 2 3 | throw.cpp: In function -F¡void catchit()¢: throw.cpp:15:7: warning: exception of type ¡Derived_Exception¢ will be caught [enabled by default] throw.cpp:13:7: warning: by earlier handler for ¡Exception¢ [enabled by default] |
幸好警告不会停止编译;我们可以继续并尝试链接结果的.o;我们将发现一个链接器错误:
throw.o:(.rodata._ZTI17Derived_Exception[typeinfo for Derived_Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__si_class_type_info'
再次的,我们就是看到__type_info错误。如果我们创建一个假的__si_class_type_info来绕过这个问题,我们将最终看到在处理继承时我们的ABI以一个相当好玩的方式崩溃了:编译器警告我们死代码,然后我们看到我们的ABI执行相同的代码!
1 2 3 4 5 6 7 8 9 10 | g++ -c -o throw.o -O0 -ggdb throw.cpp throw.cpp: In function ¡void catchit()¢: throw.cpp:15:7: warning: exception of type ¡Derived_Exception¢ will be caught [enabled by default] throw.cpp:13:7: warning: by earlier handler for ¡Exception¢ [enabled by default] gcc main.o throw.o mycppabi.o -O0 -ggdb -o app ./app begin FTW Caught a Derived_Exception! end FTW catchit handled the exception |
显然我们的ABI有问题,很容易就追踪回到can_handle的定义,检查异常是否可以被一个catch块捕捉的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | bool can_handle(const std::type_info *thrown_exception, const std::type_info *catch_type) { // If the catch has no type specifier we're dealing with a catch(...) // and we can handle this exception regardless of what it is if (not catch_type) return true; // Naive type comparisson: only check if the type name is the same // This won't work with any kind of inheritance if (thrown_exception->name() == catch_type->name()) return true; // If types don't match just don't handle the exception return false; } |
我们的ABI对要抛出的异常以及可以处理的类型获取std::type_info,然后比较这些类型的名字是否相同。这很好,只要不涉及继承,但在上面的例子里我们已经找到一个情形,即使名字不同,也应该处理异常。
在尝试捕捉一个异常指针时,出现相同的问题:名字不匹配。更有趣的,如果你尝试并链接throw.cpp,但修改catch接受一个指针,你将得到新的链接错误。如果修正它,你最终得到像这样的东西:
1 2 3 4 5 | namespace __cxxabiv1 { struct __class_type_info { virtual void foo() {} } ti; struct __si_class_type_info { virtual void foo() {} } si; struct __pointer_type_info { virtual void foo() {} } ptr; } |
一个非常有趣的模式开始浮现:对每个可能使用的catch类型有不同的*_type_info。实际上,编译器对每个抛出风格生成不同的结构。例如,对这些抛出:
1 2 | throw new Exception; throw Exception; |
编译器将生成像这样的东西:
1 2 | __cxa_throw(_Struct_Type_Info__Ptr__Exception); __cxa_throw(_Struct_Type_Info__Class__Exception); |
实际上,即使对这个简单例子,继承网(不是树,是网)也是相当复杂的(注意我在这里创造了名字重整,它不是gcc所使用的):
所以这些类由编译器生成来准确说明哪些类要抛出,以及如何。例如,如果抛出一个类型为Ptr__Type_Info_Derived_Exception的异常,catch可以处理它,如果:
- catch类型完全等于抛出类型(这是我们ABI进行的仅有检查)。
- 如果catch类型是指针(即从cxxabi::pointer_type_info继承),并宣称该指针可以强制转换到异常类型。
- 如果抛出类型是一个衍生类型,我们需要检查catch类型是否为父类型
这个列表仍然缺少了很多可能性,完整列表最好查看真正的C++ ABI。LLVM有非常清晰且容易理解的ABI。你可以在文件private_typeinfo.cpp里查看这些细节。如果查看LLVM运行时类型信息的实现,你将明白为什么我们不在我们的ABI上实现:确定两个类型是否相同的规则非常复杂。
C++异常的幕后附录III:RTTI与异常的正交性
原文地址:https://monoinfinito.wordpress.com/2013/07/25/c-exceptions-under-the-hood-appendix-iii-rtti-and-exceptions-orthogonality/
作者:nicolasbrailo
C++上异常处理要求大量的反射。我不是指程序员应该仔细思考异常处理(虽然这可能不是坏主意),我的意思是一段C++代码应该能够理解关于自身。这看起来非常像运行时类型信息,RTTI。它们是一样的吗?如果是,没有RTTI异常处理也能工作吗?
在编译我们的ABI项目时,通过在gcc上使用-fno-rtti,我们可能得到RTTI与异常处理区别的一点线索。让我们使用throw.cpp文件:
1 2 3 | g++ -fno-rtti -S throw.cpp -o throw.nortti.s g++ -S throw.cpp -o throw.s diff throw.s throw.nortti.s |
如果你自己尝试你应该看到RTTI与非RTTI版本没有区别。我们可以下结论说gcc的异常处理机制异于RTTI吗?还不行,让我们看一下,如果尝试使用RTTI会发生什么:
1 2 3 4 5 | void raise() { Exception ex; typeid(ex); throw Exception(); } |
如果你尝试编译这,gcc将抱怨:你不能使用-fno-rtti又使用typeid。这是合理的。让我们以简单的测试看一下typeid做什么:
1 2 3 4 5 6 7 8 | #include <typeinfo> class Bar {}; const std::type_info& foo() { Bar bar; return typeid(bar); } |
如果我们使用g++ -O0 -S编译此,你将看到foo被编译为像这样的东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | _Z3foov: .LFB19: # Prologue stuff... subl $16, %esp # Bar bar movl $_ZTI3Bar, %eax # typeid(bar) leave # Epilogue stuff... _ZTS3Bar: # Definition for _ZTS3Bar... _ZTI3Bar: .long _ZTVN10__cxxabiv117__class_type_infoE+8 .long _ZTS3Bar .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits |
看起来熟悉吗?如果不是,那么尝试把样例代码改为这个:
1 2 | class Bar {}; void foo() { throw Bar(); } |
像g++ -O1 -fnot-rtti -S test.cpp这样编译它,查看结果文件。现在你应该看到像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | _Z3foov: # Prologue stuff... # Initialize exception subl $24, %esp movl $1, (%esp) call __cxa_allocate_exception movl $0, 8(%esp) # Specify Bar as exception thrown movl $_ZTI3Bar, 4(%esp) movl %eax, (%esp) # Handle exception call __cxa_throw # Epilogue stuff... _ZTS3Bar: # Definition for _ZTS3Bar... _ZTI3Bar: .long _ZTVN10__cxxabiv117__class_type_infoE+8 .long _ZTS3Bar .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits |
这确实看起来熟悉:要抛出的类完全与用于typeid的类相同!
现在我们可以总结发生了什么:异常抛出类型信息实现,需要反射并依赖于它的RTTI信息,与typeid以及其他RTTI元素底下的实现完全相同。在g++上指明-fno-rtti仅禁止了RTTI的“前端”方法:这意味着你将不能使用typeid,不会生成RTTI类……除非抛出一个异常,在这个情形里将生成所需的RTTI类,不管-fno-rtti是否出现(虽然你仍然不能通过typeid访问这个类的RTTI信息)。