How expensive is RTTI?
我了解使用RTTI会带来资源损失,但是它有多大? 我看过的每个地方都说" RTTI很昂贵",但是它们都没有给出任何基准或定量的数据保护内存,处理器时间或速度。
那么,RTTI到底有多贵? 我可能会在只有4MB RAM的嵌入式系统上使用它,因此每一位都很重要。
编辑:根据S. Lott的回答,如果我包括我的实际工作,那就更好了。 我正在使用一个类来传递不同长度的数据,并且可以执行不同的操作,因此仅使用虚函数很难做到这一点。 似乎使用几个
据我了解,
无论编译器如何,只要有能力,您都可以始终节省运行时
1 2 3 4 | if (typeid(a) == typeid(b)) { B* ba = static_cast<B*>(&a); etc; } |
代替
1 2 3 4 | B* ba = dynamic_cast<B*>(&a); if (ba) { etc; } |
前者仅包含
除此之外……就像大家所说的那样,资源使用是特定于实现的。
我同意所有人的意见,即出于设计原因,提交者应避免使用RTTI。但是,有充分的理由使用RTTI(主要是因为boost :: any)。记住,在常见的实现中了解其实际资源使用情况很有用。
我最近对GCC中的RTTI做了大量研究。
tl; dr:GCC中的RTTI使用的空间可忽略不计,并且
粒度细节:
GCC倾向于使用特定的"与供应商无关"的C ++ ABI [1],并且始终将此ABI用于Linux和BSD目标[2]。对于支持此ABI和弱链接的平台,即使跨越动态链接边界,
在GCC首选的ABI中,类vtable始终持有指向每个类型RTTI结构的指针,尽管可能不使用它。因此,
据我所知,除了名称之外,GCC使用的RTTI结构(这些都是
一个快速实验(在64位Ubuntu 10.04上使用GCC 4.4.3)显示,
[1]被称为Itanium C ++ ABI,在http://www.codesourcery.com/public/cxx-abi/abi.html上有记录。这些名称非常令人困惑:尽管ABI规范适用于包括i686 / x86_64在内的许多体系结构,但该名称是指原始的开发体系结构。 GCC内部源代码和STL代码中的注释将Itanium称为"新" ABI,而之前使用的是"旧" ABI。更糟糕的是,"新" / Itanium ABI指的是通过
[2]我找不到任何平台列出的
这些数字也许会有所帮助。
我正在使用此进行快速测试:
- GCC Clock()+ XCode的探查器。
- 1亿次循环迭代。
- 2个2.66 GHz双核Intel Xeon。
- 所讨论的类是从单个基类派生的。
- typeid()。name()返回" N12fastdelegate13FastDelegate1IivEE"
测试了5例:
1 2 3 4 5 6 7 8 9 | 1) dynamic_cast< FireType* >( mDelegate ) 2) typeid( *iDelegate ) == typeid( *mDelegate ) 3) typeid( *iDelegate ).name() == typeid( *mDelegate ).name() 4) &typeid( *iDelegate ) == &typeid( *mDelegate ) 5) { fastdelegate::FastDelegateBase *iDelegate; iDelegate = new fastdelegate::FastDelegate1< t1 >; typeid( *iDelegate ) == typeid( *mDelegate ) } |
5只是我的实际代码,因为在检查它是否类似于我已经拥有的对象之前,我需要创建该类型的对象。
没有优化
结果是(我平均进行了几次运行):
1 2 3 4 5 | 1) 1,840,000 Ticks (~2 Seconds) - dynamic_cast 2) 870,000 Ticks (~1 Second) - typeid() 3) 890,000 Ticks (~1 Second) - typeid().name() 4) 615,000 Ticks (~1 Second) - &typeid() 5) 14,261,000 Ticks (~23 Seconds) - typeid() with extra variable allocations. |
因此,结论是:
-
对于没有优化的简单转换案例,
typeid() 的速度是dyncamic_cast 的两倍以上。 - 在现代机器上,两者之间的差异约为1纳秒(百万分之一毫秒)。
具有优化(-Os)
1 2 3 4 5 | 1) 1,356,000 Ticks - dynamic_cast 2) 76,000 Ticks - typeid() 3) 76,000 Ticks - typeid().name() 4) 75,000 Ticks - &typeid() 5) 75,000 Ticks - typeid() with extra variable allocations. |
因此,结论是:
-
对于具有优化效果的简单演员表,
typeid() 比dyncamic_cast 快近20倍。
图表
代码
根据注释中的要求,代码在下面(有点混乱,但是可以使用)。可从此处获得" FastDelegate.h"。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | #include <iostream> #include"FastDelegate.h" #include"cycle.h" #include"time.h" // Undefine for typeid checks #define CAST class ZoomManager { public: template < class Observer, class t1 > void Subscribe( void *aObj, void (Observer::*func )( t1 a1 ) ) { mDelegate = new fastdelegate::FastDelegate1< t1 >; std::cout <<"Subscribe "; Fire( true ); } template< class t1 > void Fire( t1 a1 ) { fastdelegate::FastDelegateBase *iDelegate; iDelegate = new fastdelegate::FastDelegate1< t1 >; int t = 0; ticks start = getticks(); clock_t iStart, iEnd; iStart = clock(); typedef fastdelegate::FastDelegate1< t1 > FireType; for ( int i = 0; i < 100000000; i++ ) { #ifdef CAST if ( dynamic_cast< FireType* >( mDelegate ) ) #else // Change this line for comparisons .name() and & comparisons if ( typeid( *iDelegate ) == typeid( *mDelegate ) ) #endif { t++; } else { t--; } } iEnd = clock(); printf("Clock ticks: %i, ", iEnd - iStart ); std::cout << typeid( *mDelegate ).name()<<" "; ticks end = getticks(); double e = elapsed(start, end); std::cout <<"Elasped:" << e; } template< class t1, class t2 > void Fire( t1 a1, t2 a2 ) { std::cout <<"Fire "; } fastdelegate::FastDelegateBase *mDelegate; }; class Scaler { public: Scaler( ZoomManager *aZoomManager ) : mZoomManager( aZoomManager ) { } void Sub() { mZoomManager->Subscribe( this, &Scaler::OnSizeChanged ); } void OnSizeChanged( int X ) { std::cout <<"Yey! "; } private: ZoomManager *mZoomManager; }; int main(int argc, const char * argv[]) { ZoomManager *iZoomManager = new ZoomManager(); Scaler iScaler( iZoomManager ); iScaler.Sub(); delete iZoomManager; return 0; } |
这取决于事物的规模。在大多数情况下,它只是几次检查和一些指针取消引用。在大多数实现中,在具有虚拟功能的每个对象的顶部,都有一个指向vtable的指针,该表保存了指向该类上虚拟功能的所有实现的指针列表。我想大多数实现都会使用它来存储指向该类的type_info结构的另一个指针。
例如在伪C ++中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct Base { virtual ~Base() {} }; struct Derived { virtual ~Derived() {} }; int main() { Base *d = new Derived(); const char *name = typeid(*d).name(); // C++ way // faked up way (this won't actually work, but gives an idea of what might be happening in some implementations). const vtable *vt = reinterpret_cast<vtable *>(d); type_info *ti = vt->typeinfo; const char *name = ProcessRawName(ti->name); } |
通常,反对RTTI的真正理由是每次添加新的派生类时都必须在各处修改代码的不可维护性。不必在各处都使用switch语句,而是将其分解为虚函数。这会将类之间所有不同的所有代码移到类本身中,因此,新的派生只需要覆盖所有虚函数即可成为功能齐全的类。如果您每次有人检查类的类型并执行不同的操作时都不得不遍历大型代码库,那么您将很快学会远离这种编程风格。
如果您的编译器允许您完全关闭RTTI,则最终的代码大小节省可能非常可观,而RAM空间却很小。编译器需要为每个带有虚函数的类生成type_info结构。如果关闭RTTI,则不需要在可执行映像中包含所有这些结构。
好吧,探查器永远不会说谎。
由于我有一个非常稳定的18-20类型层次结构,并且变化不大,所以我想知道是否仅使用一个简单的枚举成员就能达到目的,并避免RTTI所谓的"高"成本。我怀疑RTTI实际上是否比它引入的
事实证明,RTTI昂贵,比C ++中基本变量上的等效
该测试是在Apple LLVM 5.0编译器上完成的,并启用了库存优化功能(默认发布模式设置)。
因此,我有以下2个功能,每个功能都可以通过1)RTTI或2)一个简单的开关来确定对象的具体类型。它这样做了50,000,000次。事不宜迟,我向您介绍了50,000,000次运行的相对运行时间。
没错,
长话短说:如果您有足够的能力像下面所做的那样插入
就是说,这样做不会搞乱您的OOP做法。.仅在类型信息根本不可用并且您发现自己陷入使用RTTI的困境时才使用它。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | #include <stdio.h> #include <vector> using namespace std; enum AnimalClassTypeTag { TypeAnimal=1, TypeCat=1<<2,TypeBigCat=1<<3,TypeDog=1<<4 } ; struct Animal { int typeTag ;// really AnimalClassTypeTag, but it will complain at the |= if // at the |='s if not int Animal() { typeTag=TypeAnimal; // start just base Animal. // subclass ctors will |= in other types } virtual ~Animal(){}//make it polymorphic too } ; struct Cat : public Animal { Cat(){ typeTag|=TypeCat; //bitwise OR in the type } } ; struct BigCat : public Cat { BigCat(){ typeTag|=TypeBigCat; } } ; struct Dog : public Animal { Dog(){ typeTag|=TypeDog; } } ; typedef unsigned long long ULONGLONG; void dynamicCasts(vector<Animal*> &zoo, ULONGLONG tests) { ULONGLONG animals=0,cats=0,bigcats=0,dogs=0; for( ULONGLONG i = 0 ; i < tests ; i++ ) { for( Animal* an : zoo ) { if( dynamic_cast<Dog*>( an ) ) dogs++; else if( dynamic_cast<BigCat*>( an ) ) bigcats++; else if( dynamic_cast<Cat*>( an ) ) cats++; else //if( dynamic_cast<Animal*>( an ) ) animals++; } } printf("%lld animals, %lld cats, %lld bigcats, %lld dogs ", animals,cats,bigcats,dogs ) ; } //*NOTE: I changed from switch to if/else if chain void regularSwitch(vector<Animal*> &zoo, ULONGLONG tests) { ULONGLONG animals=0,cats=0,bigcats=0,dogs=0; for( ULONGLONG i = 0 ; i < tests ; i++ ) { for( Animal* an : zoo ) { if( an->typeTag & TypeDog ) dogs++; else if( an->typeTag & TypeBigCat ) bigcats++; else if( an->typeTag & TypeCat ) cats++; else animals++; } } printf("%lld animals, %lld cats, %lld bigcats, %lld dogs ", animals,cats,bigcats,dogs ) ; } int main(int argc, const char * argv[]) { vector<Animal*> zoo ; zoo.push_back( new Animal ) ; zoo.push_back( new Cat ) ; zoo.push_back( new BigCat ) ; zoo.push_back( new Dog ) ; ULONGLONG tests=50000000; dynamicCasts( zoo, tests ) ; regularSwitch( zoo, tests ) ; } |
标准方式:
1 | cout << (typeid(Base) == typeid(Derived)) << endl; |
标准RTTI昂贵,因为它依赖于进行基础字符串比较,因此RTTI的速度可能会因类名长度而异。
使用字符串比较的原因是使它跨库/ DLL边界一致地工作。如果您静态构建应用程序和/或使用某些编译器,则可以使用:
1 | cout << (typeid(Base).name() == typeid(Derived).name()) << endl; |
不能保证能正常工作(永远不会产生假阳性,但可能会产生假阴性),但可以快15倍。这依赖于typeid()的实现以某种方式工作,并且您所做的只是比较内部char指针。有时也等同于:
1 | cout << (&typeid(Base) == &typeid(Derived)) << endl; |
但是,您可以安全地使用混合动力,如果类型匹配,混合动力将非常快,对于不匹配的类型,混合动力将是最坏的情况:
1 2 | cout << ( typeid(Base).name() == typeid(Derived).name() || typeid(Base) == typeid(Derived) ) << endl; |
要了解是否需要对此进行优化,需要查看与处理该数据包所花费的时间相比,花费多少时间来获取一个新数据包。在大多数情况下,字符串比较可能不会带来很大的开销。 (取决于您的类或命名空间::类名的长度)
最安全的优化方法是将自己的typeid实现为int(或枚举Type:int)作为基础类的一部分,并使用该类型来确定类的类型,然后仅使用static_cast <>或reinterpret_cast < >
对我来说,未优化的MS VS 2005 C ++ SP1的差异大约是15倍。
最好总是进行测量。在下面的代码中,在g ++下,使用手工编码的类型标识似乎比RTTI快三倍。我敢肯定,使用字符串而不是char的更现实的手工编码实现会更慢,从而使时间紧迫。
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 27 28 29 30 31 32 33 34 35 36 37 | #include <iostream> using namespace std; struct Base { virtual ~Base() {} virtual char Type() const = 0; }; struct A : public Base { char Type() const { return 'A'; } }; struct B : public Base {; char Type() const { return 'B'; } }; int main() { Base * bp = new A; int n = 0; for ( int i = 0; i < 10000000; i++ ) { #ifdef RTTI if ( A * a = dynamic_cast <A*> ( bp ) ) { n++; } #else if ( bp->Type() == 'A' ) { A * a = static_cast <A*>(bp); n++; } #endif } cout << n << endl; } |
对于简单的检查,RTTI可以和指针比较一样便宜。对于继承检查,如果您在一个实现中从上到下进行
您也可以通过不使用
尽管那时候就像使用switch语句,所以就可以了。
不久前,我针对3GHz PowerPC在MSVC和GCC的特定情况下测量了RTTI的时间成本。在我运行的测试中(一个带有深层类树的相当大的C ++应用程序),每个
So, just how expensive is RTTI?
这完全取决于您使用的编译器。我知道有些使用字符串比较,而另一些使用实际算法。
您唯一的希望是编写一个示例程序,然后查看编译器的工作(或至少确定执行一百万个
RTTI可以很便宜,并且不需要strcmp。
编译器将测试限制为以相反的顺序执行实际的层次结构。
因此,如果您有一个类C,它是类B的子级,又是类A的子级,则从A * ptr到C * ptr的dynamic_cast意味着只有一个指针比较而不是两个指针(顺便说一句,只有vptr表指针是比较)。测试就像"如果(vptr_of_obj == vptr_of_C)返回(C *)obj"
另一个例子,如果我们尝试从A *到B *进行dynamic_cast。在这种情况下,编译器将依次检查这两种情况(obj为C,而obj为B)。这也可以简化为单个测试(大多数情况下),因为虚拟函数表是作为汇总生成的,因此测试恢复为" if(off(offset_of(vptr_of_obj,B)== vptr_of_B)")
与
offset_of =返回sizeof(vptr_table)> = sizeof(vptr_of_B)? vptr_of_new_methods_in_B:0
的内存布局
1 | vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ] |
编译器如何知道在编译时如何对此进行优化?
在编译时,编译器知道对象的当前层次结构,因此它拒绝编译不同类型的层次结构dynamic_casting。然后,它只需要处理层次结构的深度,并添加测试的反转量即可匹配该深度。
例如,这不会编译:
1 2 3 | void * something = [...]; // Compile time error: Can't convert from something to MyClass, no hierarchy relation MyClass * c = dynamic_cast<MyClass*>(something); |
RTTI可能是"昂贵的",因为您每次进行RTTI比较时都会添加一个if语句。在深层嵌套的迭代中,这可能会很昂贵。在某些永远不会循环执行的东西中,它实际上是免费的。
选择是使用适当的多态设计,消除if语句。在深度嵌套的循环中,这对于性能至关重要。否则,这并不重要。
RTTI也是昂贵的,因为它可以使子类层次结构变得模糊(即使有的话)。从"面向对象的编程"中删除"面向对象的"可能会有副作用。