RTTI 有多贵?

我知道使用 RTTI 会对资源造成冲击,但是这个冲击有多大呢?我看到的所有地方都只是说“ RTTI 是昂贵的”,但是没有一个实际上给出任何基准或者定量数据来保证内存、处理器时间或者速度。

那么 RTTI 到底有多贵?我可能会在嵌入式系统中使用它,因为我只有4MB 的内存,所以每个位都很重要。

编辑: 根据 S · 洛特的回答,如果我包括我实际上在做什么会更好。因此,仅使用虚函数很难完成这项工作。似乎使用一些 dynamic_cast可以通过允许不同的派生类通过不同的级别来解决这个问题,但是仍然允许它们完全不同的操作。

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

82883 次浏览

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

RTTI 可能是“昂贵的”,因为每次进行 RTTI 比较时都会添加一个 if 语句。在深度嵌套的迭代中,这可能是昂贵的。在一些从来没有在循环中执行过的东西中,它本质上是自由的。

可以选择使用适当的多态设计,消除 if 语句。在深度嵌套循环中,这对性能至关重要。不然的话,也没什么大不了的。

RTTI 的开销也很大,因为它可以模糊子类层次结构(如果有的话)。它的副作用是将“面向对象编程”中的“面向对象”去掉。

对于一个简单的检查,RTTI 可以像指针比较一样便宜。对于继承性检查来说,如果您在一个实现中从上到下地执行 dynamic_cast,那么对于继承树中的每种类型,它的开销可能与 strcmp一样高。

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

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

那么 RTTI 到底有多贵?

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

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

衡量事物总是最好的。在下面的代码中,在 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;
}

标准方法:

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

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

使用字符串比较的原因是为了使它跨库/DLL 边界一致地工作。如果您静态地构建应用程序,并且/或者您正在使用某些编译器,那么您可以使用:

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

这不能保证工作(将永远不会给一个假阳性,但可能会给假阴性) ,但可以高达15倍的速度。这依赖于 typeid ()的实现以某种方式工作,而您所做的只是比较一个内部字符指针。这有时也相当于:

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

然而,你 可以使用混合动力将是非常快的,如果类型匹配,将是最坏的情况下不匹配的类型:

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

为了理解是否需要优化这个过程,您需要了解与处理数据包所需的时间相比,您花费了多少时间来获取新的数据包。在大多数情况下,字符串比较可能不会造成很大的开销。(取决于您的类或名称空间: : class name 的长度)

最安全的优化方法是将自己的 typeid 实现为 int (或 enum Type: int) ,作为 Base 类的一部分,并使用它来确定类的类型,然后只需使用 static _ cast < > 或重新解释 _ cast < >

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

RTTI 可以很便宜,而且不一定需要 strcmp。 编译器将测试限制为以相反的顺序执行实际的层次结构。 因此,如果你有一个 C 类,它是 B 类的子类,而 B 类是 A 类的子类,那么 Dynamic _ cast 从 A * ptr 到 C * ptr 只意味着一个指针比较,而不是两个(顺便说一下,只比较 vptr 表指针)。这个测试类似于“ if (vptr _ of _ obj = = vptr _ of _ C) return (C *) obj”

另一个例子,如果我们尝试把 Dynamic _ cast 从 A * 转换为 B * 。在这种情况下,编译器将依次检查这两种情况(obj 是 C,obj 是 B)。这也可以简化为单个测试(大多数情况下) ,因为虚函数表是作为聚合而生成的,所以测试恢复为“ if (offr _ of (vptr _ of _ obj,B) = = vptr _ of _ B)” 和

偏移量 _ of = 返回 sizeof (vptr _ table) > = sizeof (vptr _ of _ B) ? vptr _ of _ new _ method _ in _ B: 0

的内存布局

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

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

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

例如,这不能编译:

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

不管编译器是什么,只要你能负担得起,你总是可以节省运行时间

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 (主要是因为升压: : any)。记住,了解它在通用实现中的实际资源使用情况是很有用的。

我最近在海湾合作委员会做了一些关于 RTTI 的研究。

GCC 中的 tl; dr: 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.0464位上使用 GCC 4.4.3)表明,-fno-rtti实际上是 增加的二进制大小,比一个简单的测试程序大几百个字节。这种情况在 -g-O3的组合中始终如一地发生。我不确定为什么会增加规模; 一种可能性是 GCC 的 STL 代码在没有 RTTI 的情况下表现不同(因为异常不起作用)。

被称为 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提供的 所有版本; “ old”ABI 早于这个版本。GCC 在3.0版本中采用了 Itanium/version/“ new”ABI; “ old”ABI 在2.95或更早版本中使用过,如果我没有读错它们的变更日志的话。

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

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

也许这些数字会有帮助。

我用这个做了个快速测试:

  • GCC Clock () + XCode 的 Profiler。
  • 100,000,000次循环迭代。
  • 2 x 2.66 GHz 双核 Intel 至强。
  • 有问题的类派生自单个基类。
  • Name ()返回“ N12fastregiate13FastGenerate1IivEE”

检测了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快近20倍。

图表

enter image description 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;
}

侧写师从不撒谎。

由于我有一个18-20种类型的相当稳定的层次结构,并没有发生很大的变化,我想知道是否仅仅使用一个简单的 (咒语)就可以做到这一点,并避免所谓的 RTTI 的“高”成本。我怀疑 RTTI 实际上是否比它引入的 if语句更昂贵。天啊,天啊,是吗。

结果表明,RTTI 昂贵,更多昂贵于等价的 if语句或 C + + 中基本变量上的简单 switch。所以 S.Lott 的答案并不完全正确,RTTI 需要额外的 成本,它是 没有,因为混合中只有 具有 if语句的。这是由于 RTTI 是非常昂贵的。

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

因此,我有以下两个函数,每个函数都通过1) RTTI 或2)一个简单的开关来计算出一个对象的具体类型。这种情况发生了5千万次。废话不多说,我向您展示50,000,000次运行的相对运行时。

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