167

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

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

编辑:根据 S. Lott 的回答,如果我包括我实际在做的事情会更好。 我正在使用一个类来传递不同长度的数据并且可以执行不同的操作,因此仅使用虚拟函数很难做到这一点。似乎使用几个dynamic_casts 可以通过允许不同的派生类通过不同的级别来解决这个问题,但仍然允许它们完全不同的行为。

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

4

11 回答 11

123

无论编译器如何,如果您负担得起,您总是可以节省运行时间

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(),即使跨动态链接边界,也会为每种类型返回一致且唯一的对象。您可以 test &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。更糟糕的是,“新”/Itanium ABI 指的通过-fabi-version; “旧” ABI 早于这个版本。GCC 在 3.0 版本中采用了 Itanium/versioned/"new" ABI;如果我正确阅读了他们的变更日志,则在 2.95 及更早版本中使用了“旧”ABI。

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

于 2010-12-02T11:25:59.817 回答
55

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

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

  • GCC Clock() + XCode 的 Profiler。
  • 100,000,000 次循环迭代。
  • 2 个 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.

图表

在此处输入图像描述

编码

根据评论中的要求,代码如下(有点乱,但有效)。'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;
}
于 2012-12-15T18:00:42.133 回答
40

这取决于事物的规模。在大多数情况下,它只是几个检查和一些指针取消引用。在大多数实现中,在每个具有虚函数的对象的顶部,都有一个指向 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,则所有这些结构都不需要包含在可执行映像中。

于 2009-02-23T23:56:44.680 回答
18

好吧,分析器从不撒谎。

由于我有一个非常稳定的 18-20 类型的层次结构,变化不大,我想知道是否只使用一个简单的枚举成员就可以解决问题并避免 RTTI 所谓的“高”成本。如果 RTTI 实际上比if它引入的声明更昂贵,我持怀疑态度。男孩哦,男孩,是吗。

事实证明,RTTI昂贵,C++ 中的等效if语句或简单的原始变量要昂贵得多。switch所以 S.Lott 的回答并不完全正确, RTTI需要额外的成本,这不仅仅是因为混合了一个if声明。这是因为 RTTI 非常昂贵。

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

因此,我有以下 2 个函数,每个函数都通过 1) RTTI 或 2) 一个简单的开关来确定对象的具体类型。它这样做了 50,000,000 次。事不宜迟,我将向您展示 50,000,000 次运行的相对运行时间。

在此处输入图像描述

没错,它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 ) ;
}
于 2013-10-29T02:12:45.407 回答
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 (或 enum Type : int )作为 Base 类的一部分,并使用它来确定类的类型,然后只需使用 static_cast<> 或 reinterpret_cast< >

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

于 2009-09-23T21:19:10.520 回答
8

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

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

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

于 2009-02-24T00:00:40.927 回答
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;
}
于 2009-02-24T15:11:26.917 回答
4

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

于 2010-12-02T11:32:51.163 回答
2

那么,RTTI 到底有多贵?

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

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

于 2009-02-24T01:12:20.880 回答
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 ]

编译器如何知道在编译时对其进行优化?

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

例如,这不会编译:

void * something = [...]; 
// Compile time error: Can't convert from something to MyClass, no hierarchy relation
MyClass * c = dynamic_cast<MyClass*>(something);  
于 2010-08-26T13:27:20.513 回答
-4

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

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

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

于 2009-02-23T23:56:57.863 回答