RTTI的成本高吗?

173

我知道使用RTTI会有资源开销,但它的大小如何? 我在网上查找了很多地方都只是说“RTTI很昂贵”,但没有一个给出任何关于内存、处理器时间或速度的基准或量化数据。

那么,RTTI到底有多昂贵? 我可能会在仅有4MB RAM的嵌入式系统上使用它,所以每一位都很重要。

编辑:根据S.Lott的回答,最好是我包括我实际做的事情。 我正在使用一个类来传递不同长度的数据并执行不同的操作,因此仅使用虚函数将很困难。 使用一些dynamic_cast似乎可以通过允许不同的派生类通过不同的级别以完全不同的方式运行,来解决这个问题。

据我所知,dynamic_cast使用RTTI,所以我想知道在有限的系统上使用它是否可行。


3
根据你的编辑 - 很多时候当我发现自己在进行多个动态转换时,意识到使用访问者模式可以再次使事情变得清晰起来。这对你有用吗? - philsquared
4
我这么说吧——我刚开始在C++中使用dynamic_cast,现在每10次中就有9次我会用调试器“破坏”程序,它会断在内部的dynamic_cast函数里。很慢。 - user541686
5
顺便提一下,RTTI的意思是“运行时类型信息”。 - Noumenon
13个回答

125

无论编译器如何,如果你负担得起,总是能在运行时节省时间。

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

取代

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

前者仅涉及一次std::type_info的比较;而后者必然涉及遍历继承树以及比较。

除此之外,正如每个人所说,资源使用是与实现相关的。

我同意其他人的评论,即提交者应出于设计原因避免使用RTTI。然而,存在使用RTTI的好理由(主要是因为boost::any)。考虑到这一点,了解常见实现中其实际资源使用情况很有用。

我最近对GCC中的RTTI进行了大量研究。

总结一下:在许多平台上(Linux、BSD和可能的嵌入式平台,但不包括mingw32),GCC中的RTTI占用空间可以忽略不计,而typeid(a) == typeid(b)非常快速。如果您知道您将始终在受保护的平台上运行,则RTTI几乎是免费的。

详细信息如下:

GCC倾向于使用特定的“供应商中立”C++ ABI[1],并始终针对Linux和BSD目标使用该ABI[2]。对于支持此ABI并且还具有弱链接的平台,typeid()返回每种类型的一致且唯一的对象,即使跨动态链接边界也是如此。您可以测试&typeid(a) == &typeid(b),或者仅仅依赖于可移植的测试typeid(a) == typeid(b)实际上只是在内部比较指针。

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

从我了解到的情况来看,GCC使用的RTTI结构(这些都是std::type_info的子类)除了名称之外,每种类型只包含少量字节。不清楚名称是否存在于输出代码中,即使使用了-fno-rtti。无论哪种方式,编译后的二进制文件大小的更改都应反映运行时内存使用情况的更改。

一个快速的实验(使用Ubuntu 10.04 64位上的GCC 4.4.3)显示,对于简单的测试程序,-fno-rtti实际上会使二进制文件大小增加几百字节。这在-g-O3的组合中始终如一。我不确定为什么大小会增加;有可能是因为没有RTTI,GCC的STL代码的行为不同(因为异常无法工作)。

[1]称为Itanium C++ ABI,在http://www.codesourcery.com/public/cxx-abi/abi.html上记录。名称非常令人困惑:名称是指原始开发架构,尽管ABI规范适用于许多架构,包括i686/x86_64。GCC内部源代码和STL代码中的注释将Itanium称为“新”ABI,与它们之前使用的“旧”ABI相对应。更糟糕


8
异常处理不需要运行时类型信息(RTTI)。你可以抛出一个“int”类型的异常,而这种情况下不需要虚函数表(vtable):) - Billy ONeal
5
抱歉让您失望,但当我在编译器中关闭运行时类型信息(RTTI)时,它们能够正常工作。 - Billy ONeal
7
异常处理机制必须能够与满足一些基本要求的任何类型配合使用。您可以自由地建议如何处理在模块边界上抛出和捕获任意类型的异常,而不需要运行时类型信息(RTTI)。请考虑到需要向上和向下转换。 - Deduplicator
23
typeid(a) == typeid(b)并不等同于B* ba = dynamic_cast<B*>(&a),在具有多重继承的对象上进行测试,您会发现typeid()==typeid()将不会返回正确结果。dynamic_cast是搜索继承树的唯一方法。停止考虑通过禁用RTTI来实现潜在的节省,并直接使用它。如果您的代码太臃肿,则优化它。尽量避免在内部循环或任何其他性能关键代码中使用dynamic_cast,这样就可以避免问题。 - mysticcoder
4
这就是为什么文章明确说明“后者必须涉及遍历继承树以及比较”的原因。当你不需要支持从整个继承树进行强制类型转换时,你可以“负担得起”这样做。例如,如果你想在集合中找到所有类型为X的项目,但不包括从X派生的项目,则应该使用前者。如果你需要找到所有派生实例,那么你将不得不使用后者。 - Aidiakapi
显示剩余8条评论

57
也许这些数字可以帮助您了解。
我使用以下内容进行了快速测试:
GCC Clock() + XCode的Profiler。
100,000,000个循环迭代。
2 x 2.66 GHz 双核Intel Xeon。
所涉及的类派生自一个基类。
typeid().name()返回“N12fastdelegate13FastDelegate1IivEE”。
测试了5种情况:
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)  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() 比使用 dynamic_cast 快近 20 倍。

图表

enter image description here

代码

根据评论的要求,下面是代码(有点凌乱,但可以运行)。'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;
}

2
当然,动态转换更通用--如果项目更派生,则它可以工作。例如,class a {}; class b : public a {}; class c : public b {}; 当目标是 c 类的实例时,使用 dynamic_cast 测试 b 类将正常工作,但使用 typeid 解决方案则不行。尽管如此,这仍然是合理的,+1。 - Billy ONeal
43
这个基准测试完全是虚假的,使用了优化技巧:typeid检查是循环不变量,被移出了循环。这并不有趣,这是一个基本的基准测试错误。 - Kuba hasn't forgotten Monica
3
@Kuba:那这个基准测试就是虚假的。这不是关闭优化进行基准测试的理由;而是应该写出更好的基准测试来解决问题。 - Billy ONeal
3
再次失败了。“对于使用优化的简单转换情况,typeid() 几乎比 dynamic_cast 快20倍。” 它们并不做相同的事情。dynamic_cast 慢的原因是有其理由的。 - mysticcoder
1
@KubaOber:总共+1。这很经典。从循环数量的外观上看,这应该是显而易见的。 - v.oddou
显示剩余4条评论

41

这取决于事物的规模。在大多数情况下,只需要进行几个检查和几个指针解引用操作。在大多数实现中,对于每个具有虚函数的对象,都会在其顶部存储一个指向vtable的指针,该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的真正原因是每次添加新的派生类时需要修改代码的不可维护性。而不是到处使用switch语句,将其分解为虚函数。这将所有在类之间不同的代码移动到类本身中,因此新的派生只需要覆盖所有虚函数即可成为完全功能的类。如果你曾经不得不在大量代码中查找每次检查类类型并执行不同操作的情况,你很快就会学会远离这种编程风格。
如果您的编译器允许您完全关闭RTTI,则最终生成的代码大小节省可以非常显著,特别是在如此小的RAM空间上。编译器需要为每个具有虚函数的类生成一个type_info结构。如果关闭RTTI,则所有这些结构都不需要包含在可执行映像中。

5
+1 是因为实际上解释了为什么使用 RTTI 被认为是一个糟糕的设计决策,这在之前对我来说并不清楚。 - jake
8
这个答案对于C++的能力只有初步的理解。频繁使用“通常情况下”和“大多数实现”的说法表明您并没有考虑如何充分利用语言特性。虚函数和重新实现RTTI并不是解决方法,RTTI才是。有时候你只想知道一个对象是否是某种类型,这就是它存在的原因!所以你会失去一些KB的内存用于一些type_info结构体。唉... - mysticcoder

19

好吧,分析工具从不说谎。

由于我有一个相当稳定的18-20个类型的层次结构,这些类型变化不大,我想知道是否只使用一个简单的枚举成员就可以避免RTTI的所谓“高”成本。 我怀疑RTTI是否比引入的if语句更昂贵。哇塞,它是的。

事实证明,RTTI是昂贵的,比C ++中基本类型变量上等效的if语句或简单switch语句要昂贵得多。因此,S.Lott的答案并不完全正确,RTTI确实有额外的成本,而且这不仅仅是混合中的if语句。这是因为RTTI非常昂贵。

这项测试是在Apple LLVM 5.0编译器上进行的,启用默认发布模式设置(默认优化设置)。

所以,我有以下2个函数,每个函数都通过1)RTTI或2)简单switch来找出对象的具体类型。 它执行了5000万次。 没有更多的废话,我向您呈现了5000万次运行的相对运行时间。

enter image description here

没错,dynamicCasts花费了94%的运行时间。 然而regularSwitch块只占3.3%

长话短说:如果您像我一样有足够的精力来挂接一个enum 的类型,我可能会建议使用它,如果您需要进行RTTI并且性能至关重要。 只需将成员设置一次(确保通过所有构造函数获取它),然后确保不再写入它。

也就是说,这样做不应该破坏您的OOP实践..它仅用于在类型信息不可用且您被迫使用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时采用的方法。但我将类型放在虚函数getter中,直接返回类型。这本质上是静态程序内存,不会为每个实例占用内存。 - JMS Creator

15

标准方式:

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

标准运行时类型信息(RTTI)很昂贵,因为它依赖于进行底层字符串比较,因此RTTI的速度可能会根据类名称长度的不同而有所变化。

使用字符串比较的原因是为了使其跨库/DLL边界一致工作。如果您静态构建应用程序和/或使用某些编译器,则可能可以使用:

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

这种方法不能保证总是有效(永远不会给出错误的结果,但可能会漏掉一些情况),但速度最多可以快15倍。它依赖于typeid()的实现方式,只是比较了一个内部的char指针。有时这也等同于:

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

然而,您可以使用混合类型,如果类型匹配,则速度非常快,如果不匹配,则最坏情况如下:

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倍。

4
"Standard RTTI is expensive because it relies on doing a underlying string compare" 的意思是标准的运行时类型识别(RTTI)是昂贵的,因为它依赖于执行底层的字符串比较。但实际上这并没有什么“标准”,这只是取决于你的实现中 typeid::operator 的工作方式。例如,在支持的平台上,GCC 已经使用了 char * 比较,而不需要我们强制指定 - https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01094_source.html 。当然,对于在您的平台上默认表现不佳的 MSVC,您的方法会使其表现得更好,我也不知道哪些“一些目标”本身使用指针... 但我的观点是,MSVC 的行为并不是任何意义上的“标准”。 - underscore_d

9

对于简单的检查,RTTI 可以像指针比较一样便宜。对于继承检查,如果你在一个实现中从顶部到底部使用 dynamic_cast,则可以像每个继承树类型的 strcmp 一样昂贵。

您还可以通过不使用 dynamic_cast 并通过 &typeid(...)==&typeid(type) 显式检查类型来减少开销。虽然这对于 .dll 或其他动态加载的代码不一定适用,但对于静态链接的内容来说可能非常快。

尽管此时就像使用 switch 语句一样,所以你可以选择使用哪种方式。


1
你有没有关于strcmp版本的参考资料?使用strcmp进行类型检查似乎极其低效且不准确。 - JaredPar
在一个糟糕的实现中,每种类型可能有多个type_info对象,它可以将bool type_info :: operator ==(const type_info&x)const实现为“!strcmp(name(),x.name())”。 - Greg Rogers
3
进入MSVC中dynamic_cast或typeid().operator==的反汇编代码,你会遇到一个strcmp函数。我猜这个函数用于处理一种糟糕的情况,即比较来自另一个.dll文件编译的类型。它使用了名称修饰(mangled name), 所以至少在同样的编译器下是正确的。 - MSN
1
你应该使用 "typeid(...)==typeid(type)" 而不是比较地址。 - Johannes Schaub - litb
1
我的观点是,您可以使用&typeid(...) == &typeid(blah)作为早期退出,并且是安全的。它可能实际上并没有做任何有用的事情,因为typeid(...)可能在堆栈上生成,但如果它们的地址相等,则它们的类型相等。 - MSN
为什么不能在动态链接库中使用typeid? - Ari

6

测量事物总是最好的。在下面的代码中,在g++下,手动编码类型识别的使用似乎比RTTI快三倍左右。我相信,如果使用字符串而不是字符来实现更现实的手工编码实现,速度会变慢,从而使计时接近。

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

1
尽量不要使用dynamic_cast,而是使用typeid。这样可以加快性能。 - Johannes Schaub - litb
1
但是使用dynamic_cast更加现实,至少从我的代码来看。 - anon
2
它执行不同的操作:它还检查bp是否指向从A派生的类型。your == 'A'检查它是否恰好指向'A'。我也认为这个测试有点不公平:编译器可以轻松地看到bp不能指向除A以外的任何东西。但我认为它在这里没有进行优化。 - Johannes Schaub - litb
尽管在这里使用typeid进行更改没有任何区别(仍然是0.016秒)。 - Johannes Schaub - litb
@cristian 无论哪种情况,用于类型信息的金额都非常小。 - anon
显示剩余2条评论

4

之前我在一个3ghz的PowerPC上,对MSVC和GCC在RTTI(运行时类型信息)特定情况下的时间成本进行了测量。在我所运行的测试中(一个拥有深层次类树的相当大的C++应用程序中),每个dynamic_cast<>操作的耗时在0.8μs至2μs之间,具体取决于它是否命中或未命中。


2
RTTI可以很便宜,不一定需要strcmp。编译器将测试限制为按相反顺序执行实际层次结构。因此,如果您有一个类C是类B的子类,而类B是类A的子类,则从A * ptr到C * ptr的dynamic_cast仅意味着进行一次指针比较而不是两次(顺便说一句,只有vptr表指针被比较)。 测试就像“if(vptr_of_obj == vptr_of_C)return(C *)obj”一样。
另一个例子,如果我们尝试从A *转换为B *的dynamic_cast。在这种情况下,编译器将轮流检查两种情况(obj是C和obj是B)。这也可以简化为单个测试(大多数情况下),因为虚函数表是作为聚合物制成的,因此测试恢复为“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 ]

编译器如何在编译时优化此过程?

在编译时,编译器知道当前对象的层次结构,因此它会拒绝编译不同类型层次结构的动态转换。然后,它只需处理层次深度,并添加相反数量的测试来匹配这样的深度。

例如,以下内容无法编译:

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

2

那么,RTTI(运行时类型识别)到底有多昂贵?

这完全取决于您使用的编译器。我了解到有些编译器使用字符串比较,而另一些则使用真实算法。

您唯一的希望是编写一个示例程序并查看您的编译器执行什么操作(或者至少确定执行一百万个 dynamic_casts 或一百万个 typeid 需要多长时间)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接