RTTI有多贵?

时间:2009-02-23 23:46:21

标签: c++ performance rtti

我知道使用RTTI会造成资源损失,但它有多大?我看到的每个地方都只是说“RTTI很贵”,但它们实际上都没有给出任何基准或定量数据来控制内存,处理器时间或速度。

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

编辑:As per S. Lott's answer,如果我包含我实际正在做的事情会更好。 I am using a class to pass in data of different lengths and that can perform different actions,因此仅使用虚函数很难做到这一点。似乎使用一些dynamic_cast可以通过允许不同的派生类通过不同的级别但仍然允许它们以完全不同的方式行动来解决这个问题。

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

11 个答案:

答案 0 :(得分:110)

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

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进行了一系列研究。

tl; dr:GCC中的RTTI使用的空间可以忽略不计,typeid(a) == typeid(b)非常快,在许多平台上(Linux,BSD和嵌入式平台,但不是mingw32)。如果你知道你将永远处于一个幸福的平台上,那么RTTI非常接近免费。

坚韧不拔的细节:

GCC更喜欢使用特定的“供应商中立”C ++ ABI [1],并始终将此ABI用于Linux和BSD目标[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的组合中一致发生。我不确定为什么尺寸会增加;一种可能性是GCC的STL代码在没有RTTI的情况下表现不同(因为异常不起作用)。

[1]被称为Itanium C ++ ABI,记录在http://www.codesourcery.com/public/cxx-abi/abi.html。名称非常令人困惑:名称指的是原始开发体系结构,尽管ABI规范适用于许多体系结构,包括i686 / x86_64。 GCC的内部源和STL代码中的注释将Itanium称为“新”ABI,而不是之前使用的“旧”ABI。更糟糕的是,“new”/ Itanium ABI指的是通过-fabi-version提供的所有版本; “旧”ABI早于此版本。 GCC在3.0版中采用了Itanium / versioned /“new”ABI;如果我正确地阅读他们的更改日志,那么“旧”ABI在2.95及更早版本中被使用。

[2]我无法通过平台找到任何列出std::type_info对象稳定性的资源。对于我有权访问的编译器,我使用了以下内容:echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES。从GCC 3.0开始,此宏控制GCC STL中operator==的{​​{1}}行为。我确实发现mingw32-gcc遵循Windows C ++ ABI,其中std::type_info个对象不是唯一的DLL类型; std::type_info调用了typeid(a) == typeid(b)。我推测在像AVR这样的单程序嵌入式目标上,没有可链接的代码,strcmp对象总是稳定的。

答案 1 :(得分:44)

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

我正在使用它进行快速测试:

  • 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()dyncamic_cast快近x20。

图表

enter image description here

代码

根据评论中的要求,代码如下(有点乱,但有效)。 'FastDelegate.h'可从here获得。

#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 :(得分:37)

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

例如在pseudo-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,则所有这些结构都不需要包含在可执行映像中。

答案 3 :(得分:15)

好吧,探查器永远不会说谎。

由于我有一个非常稳定的18-20种类型的层次结构并没有太大的变化,我想知道是否只使用一个简单的 enum'd成员就能做到这一点并避免据称“高” “RTTI的成本。如果RTTI实际上比它引入的if语句更昂贵,我持怀疑态度。男孩哦,男孩,是吗。

事实证明,RTTI 昂贵,更多比同等if语句或原始变量上的简单switch贵C ++。因此,S.Lott的答案并不完全正确, 对于RTTI来说是的额外成本,并且不是,因为只有if语句在混合中。这是因为RTTI非常昂贵。

此测试在Apple LLVM 5.0编译器上完成,打开了库存优化(默认发布模式设置)。

所以,我有2个以下的函数,每个函数通过1)RTTI或2)简单的开关来计算对象的具体类型。它这样做了50,000,000次。不用多说,我向你展示50,000,000次运行的相对运行时间。

enter image description here

没错,dynamicCasts占用了 94%的运行时。虽然regularSwitch区块只占 3.3%

长话短说:如果你能够负担得起我在下面做enum'd类型的能量,我可能会推荐它,如果你需要做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 ) ;
}

答案 4 :(得分:13)

标准方式:

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)作为Base类的一部分,并使用它来确定类的类型,然后使用static_cast&lt;&gt ;或reinterpret_cast&lt;&gt;

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

答案 5 :(得分:6)

对于简单的检查,RTTI可以像指针比较一样便宜。对于继承检查,对于继承树中的每个类型,如果strcmp在一个实现中从上到下,它就像dynamic_cast一样昂贵。

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

虽然在那时它就像使用switch语句,所以你去吧。

答案 6 :(得分: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;
}

答案 7 :(得分:4)

前一段时间我测量了3Vz PowerPC的MSVC和GCC特定情况下RTTI的时间成本。在我运行的测试中(一个带有深类树的相当大的C ++应用程序),每个dynamic_cast<>的成本在0.8μs到2μs之间,具体取决于它是命中还是错过。

答案 8 :(得分:2)

  

那么,RTTI有多贵?

这完全取决于您正在使用的编译器。我知道有些人使用字符串比较,而其他人使用真实的算法。

您唯一的希望是编写一个示例程序,看看您的编译器做了什么(或者至少确定执行一百万dynamic_casts或一百万typeid s所需的时间。

答案 9 :(得分:1)

RTTI可以很便宜,并且不一定需要strcmp。 编译器将测试限制为以相反的顺序执行实际层次结构。 因此,如果您的类C是B类的子类,它是A类的子类,则从A * ptr到C * ptr的dynamic_cast只表示一个指针比较,而不是两个(BTW,只有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 =返回sizeof(vptr_table)&gt; = 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 ]

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

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

例如,这不会编译:

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

答案 10 :(得分:-4)

RTTI可能“昂贵”,因为您每次进行RTTI比较时都添加了一个if语句。在深度嵌套的迭代中,这可能是昂贵的。在一个永远不会在循环中执行的东西中,它基本上是免费的。

选择是使用适当的多态设计,消除if语句。在深层嵌套循环中,这对性能至关重要。否则,它并不重要。

RTTI也很昂贵,因为它可能会模糊子类层次结构(如果有的话)。它可以具有从“面向对象编程”中去除“面向对象”的副作用。