RTTI有多贵?

我知道有使用RTTI的资源,但它有多大? 我所看到的任何地方都只是说“RTTI很昂贵”,但是他们都没有提供任何基准或定量数据来调节内存,处理器时间或速度。

那么,RTTI有多贵? 我可能在一个只有4MB内存的embedded式系统上使用它,所以每一个位都是重要的。

编辑: 根据S.罗特的答案 ,如果我包括我实际上在做什么会更好。 我正在使用一个类来传递不同长度的数据,并且可以执行不同的操作 ,所以使用虚拟函数很难做到这一点。 似乎使用一些dynamic_cast可以解决这个问题,允许不同的派生类通过不同的级别,但仍然允许他们完全不同的行为。

从我的理解, dynamic_cast使用RTTI,所以我想知道在一个有限的系统上使用是多么可行。

无论编译器如何,如果您能负担得起,您总是可以在运行时保存

 if (typeid(a) == typeid(b)) { B* ba = static_cast<B*>(&a); etc; } 

代替

 B* ba = dynamic_cast<B*>(&a); if (ba) { etc; } 

前者只涉及std::type_info一个比较; 后者必然涉及遍历一个inheritance树加上比较。

过去,就像大家所说的,资源使用是特定于实现的。

我同意其他人的意见,提交者应该避免RTTI出于devise原因。 但是,使用RTTI 很好的理由(主要是因为boost :: any)。 记住,在常见实现中了解其实际资源使用情况是有用的。

我最近在GCC做了一些RTTI的研究。

tl; dr:GCC中的RTTI使用可忽略的空间, typeid(a) == typeid(b)在很多平台(Linux,BSD和可能的embedded式平台,但不是mingw32)上速度非常快。 如果你知道你会永远在一个幸运的平台上,RTTI非常接近免费。

坚韧的细节:

GCC倾向于使用特定的“厂商中立”的C ++ ABI [1],并且总是将这个ABI用于Linux和BSD目标[2]。 对于支持这个ABI以及弱连接的平台, typeid()为每个types返回一个一致且唯一的对象,即使在dynamic链接边界也是如此。 你可以testing&typeid(a) == &typeid(b) ,或者只是依靠便携式testingtypeid(a) == typeid(b)实际上只是在内部比较一个指针的事实。

在GCC的首选ABI中,类vtable 始终保存一个指向每类RTTI结构的指针,尽pipe它可能不被使用。 因此, typeid()调用本身应该与其他的vtable查找一样花费(与调用虚拟成员函数相同),并且RTTI支持不应该为每个对象使用任何额外的空间。

从我可以做出来的,GCC使用的RTTI结构(这些都是std::type_info所有子类)只能为每个types保留几个字节,除了名称。 即使使用-fno-rtti ,输出代码中是否存在名称也不清楚。 无论哪种方式,编译二进制文件大小的变化都应该反映运行时内存使用情况的变化。

一个快速的实验(在Ubuntu 10.04 64位上使用GCC 4.4.3)表明-fno-rtti实际上将一个简单的testing程序的二进制大小增加了几百字节。 这在-g-O3组合中一致发生。 我不确定为什么尺寸会增加, 一种可能性是GCC的STL代码在没有RTTI的情况下performance不同(因为exception将不起作用)。

[1]被称为Itanium C ++ ABI,logging在http://www.codesourcery.com/public/cxx-abi/abi.html 。 这些名字是非常令人难以置信的:这个名字是指原来的开发架构,尽pipeABI规范在许多架构上都有效,包括i686 / x86_64。 GCC的内部源代码和STL代码中的评论将Itanium称为“新”ABI,而不是之前使用的“旧”ABI。 更糟糕的是,“新”/安腾ABI是指通过-fabi-version版本提供的所有版本; “旧”ABI早于此版本。 GCC在3.0版采用了安腾/版本/“新”ABI; 如果我正确地阅读他们的更新日志,那么在2.95及更早的版本中使用“旧”ABI。

[2]我找不到任何资源列表std::type_info对象稳定的平台。 对于我有权访问的编译器,我使用了以下命令: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES 。 这个macros控制GCC STL中std::type_infooperator==的行为,如GCC 3.0。 我发现mingw32-gcc服从Windows C ++ ABI,其中std::type_info对象对于跨越DLL的types是不唯一的; typeid(a) == typeid(b)在封面下调用strcmp 。 我推测,在像AVR这样的单程序embedded式目标中,没有可以链接的代码, std::type_info对象总是稳定的。

这取决于事物的规模。 大部分情况只是一些检查和一些指针解引用。 在大多数实现中,在每个具有虚函数的对象的顶部,都有一个指向vtable的指针,该指针包含指向该类虚函数的所有实现的指针列表。 我猜测大多数实现会使用这个来存储指向类的type_info结构的另一个指针。

例如在伪c ++中:

 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的真正理由是每次添加新的派生类时都不得不随时修改代码的不可维护性。 而不是到处都是开关语句,把它们分成虚拟函数。 这将所有类之间不同的代码移动到类本身,这样一个新的派生只需要覆盖所有的虚函数就可以成为一个function完备的类。 如果每次有人检查一个class级的types并做一些不同的事情时,你都不得不寻找一个庞大的代码库,那么你很快就会学会远离那种编程风格。

如果你的编译器可以让你完全closuresRTTI,那么最终的代码大小的节省将会很大,尽pipe这样的RAM空间很小。 编译器需要为每个具有虚函数的类生成一个type_info结构。 如果closuresRTTI,则所有这些结构不需要包含在可执行映像中。

也许这些数字会有所帮助。

我正在使用这个快速testing:

  • GCC时钟()+ XCode的分析器。
  • 100,000,000次循环迭代。
  • 2个2.66 GHz双核Intel Xeon。
  • 有问题的类是从一个基类派生的。
  • typeid()。name()返回“N12fastdelegate13FastDelegate1IivEE”

5个案例进行了testing:

 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只是我的实际代码,因为我需要在检查它是否类似于我已经有的types之前创build该types的对象。

没有优化

结果是(我已经平均了几次运行):

 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) 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快近dyncamic_cast

图表

在这里输入图像描述

代码

根据评论的要求,代码是低于(有点混乱,但工程)。 'FastDelegate.h'可以从这里获得 。

 #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\n"; 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,\n", iEnd - iStart ); std::cout << typeid( *mDelegate ).name()<<"\n"; 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\n"; } 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!\n"; } private: ZoomManager *mZoomManager; }; int main(int argc, const char * argv[]) { ZoomManager *iZoomManager = new ZoomManager(); Scaler iScaler( iZoomManager ); iScaler.Sub(); delete iZoomManager; return 0; } 

标准方式:

 cout << (typeid(Base) == typeid(Derived)) << endl; 

标准的RTTI是昂贵的,因为它依赖于一个底层的string比较,因此RTTI的速度可以根据类名长度而变化。

使用string比较的原因是为了使其在库/ DLL边界上一致地工作。 如果您静态构build您的应用程序和/或您正在使用某些编译器,那么您可以使用:

 cout << (typeid(Base).name() == typeid(Derived).name()) << endl; 

哪一个是不能保证工作的(永远不会给出误报,但可能会给出错误的否定),但速度可以提高15倍。 这依赖于typeid()的实现以某种方式工作,你所做的只是比较一个内部的char指针。 这也有时相当于:

 cout << (&typeid(Base) == &typeid(Derived)) << endl; 

但是,您可以安全地使用混合动力,如果types匹配,则混合动力将会非常快,而对于不匹配的types,则是最糟糕的情况:

 cout << ( typeid(Base).name() == typeid(Derived).name() || typeid(Base) == typeid(Derived) ) << endl; 

为了理解你是否需要优化这个,你需要看看你花费了多less时间来获得一个新的数据包,而不是处理数据包的时间。 在大多数情况下,string比较可能不会是一个很大的开销。 (取决于你的类或命名空间::类名称长度)

最优化的最安全的方法是将自己的typeid作为inttypes(或枚举types:int)实现为Base类的一部分,并使用它来确定类的types,然后使用static_cast <>或reinterpret_cast < >

对于我来说,未经优化的MS VS 2005 C ++ SP1的差别大约是15倍。

那么,剖析器永远不会说谎。

因为我有一个稳定的18-20types的层次结构,并没有太大的改变,所以我想知道是否使用简单的enum'd成员可以做到这一点,并避免RTTI的“高”成本。 我怀疑RTTI实际上if比它引入的if语句更昂贵。 男孩哦,男孩,是吗?

事实certificate,RTTI 昂贵的,比等价的if语句贵得多,或者在C ++中简单地switch原语variables。 所以S.Lott的答案并不完全正确,RTTI还有额外的成本,而且这不是因为只有一个if语句 。 这是由于RTTI非常昂贵。

此testing是在Apple LLVM 5.0编译器上完成的,开启了股票优化(默认释放模式设置)。

所以,我有2个以下的function,每个function都通过1)RTTI或2)一个简单的开关来指出对象的具体types。 它做了5000万次。 我毫不犹豫地向您介绍了50,000,000次运行的相对运行时间。

在这里输入图像描述

没错,dynamicdynamicCasts占用了运行时间的94% 。 而regularSwitch块只占了3.3%

长话短说:如果你能负担得起像下面那样挂接enumtypes的能量,我可能会推荐它,如果你需要做RTTI 并且性能是最重要的。 它只需要设置成员一次 (确保通过所有构造函数获取),并且确保以后不要写入成员。

也就是说, 这样做不应该搞乱你的OOP操作。它只是用来当types信息根本不可用,你发现自己被迫使用RTTI。

 #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\n", 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\n", 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 ) ; } 

对于一个简单的检查,RTTI可以像指针比较那样便宜。 对于inheritance检查,如果你在一个实现中从上到下dynamic_cast -ing,那么对于inheritance树中的每个types,它可以像strcmp那样昂贵。

您还可以通过不使用dynamic_cast来减less开销,而是通过&typeid(…)==&typeid(type)显式检查types。 虽然这不一定适用于.dlll或其他dynamic加载的代码,但对于静态链接的东西来说,它可能非常快。

虽然在这一点上,就像使用switch语句,所以你去。

衡量事物总是最好的。 在下面的代码中,在g ++下,使用手编码types识别似乎比RTTI快大约三倍。 我敢肯定,一个更现实的使用string而不是字符的手工编码实现将会变慢,从而使计时紧密在一起。

 #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; } 

前一段时间我测量了在MSVC和GCC的3ghz PowerPC的具体情况下RTTI的时间成本。 在我运行的testing中(一个相当大的C ++应用程序和一个深层的类树),每个dynamic_cast<>花费在0.8μs和2μs之间,取决于是否命中。

那么,RTTI有多贵?

这完全取决于你正在使用的编译器。 我明白,有些使用string比较,其他人使用真正的algorithm。

你唯一的希望就是编写一个示例程序,看看你的编译器做了什么(或者至less确定执行一百万个dynamic_casts或者一百万个typeid需要多less时间)。

RTTI可以很便宜,并不一定需要strcmp。 编译器将testing限制为按照相反的顺序执行实际的层次结构。 所以如果你有一个C类的子类,它是类A的一个子类,从A * ptr到C * ptr的dynamic_cast只暗示一个指针比较而不是两个(顺便提一下,只有vptr表指针是相比)。 testing就像“if(vptr_of_obj == vptr_of_C)return(C *)obj”

另一个例子,如果我们尝试从A *到B *的dynamic_cast。 在这种情况下,编译器会轮stream检查case(obj是C,obj是B)。 这也可以简化为单个testing(大部分时间),因为虚函数表是一个聚集,所以testing恢复到“if(offset_of(vptr_of_obj,B)== vptr_of_B)”

offset_of = return sizeof(vptr_table)> = sizeof(vptr_of_B)? vptr_of_new_methods_in_B:0

内存布局

 vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ] 

编译器如何知道在编译时优化它?

在编译时,编译器知道当前对象的层次结构,所以它拒绝编译不同types的层次结构dynamic_casting。 然后它只需要处理层次深度,并添加反转量的testing来匹配这样的深度。

例如,这不会编译:

 void * something = [...]; // Compile time error: Can't convert from something to MyClass, no hierarchy relation MyClass * c = dynamic_cast<MyClass*>(something); 

RTTI可能是“昂贵的”,因为您每次执行RTTI比较时都添加了if语句。 在深度嵌套迭代中,这可能是昂贵的。 从来没有在循环中执行的东西,它本质上是免费的。

select是使用适当的多态devise,消除if语句。 在深度嵌套循环中,这对性能至关重要。 否则,这并不重要。

RTTI也是昂贵的,因为它可能掩盖子类层次(如果有的话)。 它可以从“面向对象编程”中消除“面向对象”的副作用。