关于c ++:RTTI的价格是多少?

 2020-02-14 

How expensive is RTTI?

我了解使用RTTI会带来资源损失,但是它有多大? 我看过的每个地方都说" RTTI很昂贵",但是它们都没有给出任何基准或定量的数据保护内存,处理器时间或速度。

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

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

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


无论编译器如何,只要有能力,您都可以始终节省运行时

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

代替

1
2
3
4
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,名称是否也存在于输出代码中。无论哪种方式,已编译二进制文件的大小更改都应反映运行时内存使用情况的更改。

一个快速实验(在64位Ubuntu 10.04上使用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。更糟糕的是,"新" / Itanium ABI指的是通过-fabi-version可用的所有版本。"旧" ABI早于此版本。 GCC在版本3.0中采用了Itanium /版本/"新" ABI;如果我正确读取了他们的变更日志,则在2.95及更早版本中使用了"旧" ABI。

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


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

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

  • GCC Clock()+ XCode的探查器。
  • 1亿次循环迭代。
  • 2个2.66 GHz双核Intel Xeon。
  • 所讨论的类是从单个基类派生的。
  • typeid()。name()返回" N12fastdelegate13FastDelegate1IivEE"

测试了5例:

1
2
3
4
5
6
7
8
9
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
2
3
4
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
2
3
4
5
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

代码

根据注释中的要求,代码在下面(有点混乱,但是可以使用)。可从此处获得" FastDelegate.h"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#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
"
;
        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,
"
, iEnd - iStart );

        std::cout << typeid( *mDelegate ).name()<<"
"
;

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

    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!
"
;        
    }
private:
    ZoomManager *mZoomManager;
};

int main(int argc, const char * argv[])
{
    ZoomManager *iZoomManager = new ZoomManager();

    Scaler iScaler( iZoomManager );
    iScaler.Sub();

    delete iZoomManager;

    return 0;
}


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

例如在伪C ++中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,则不需要在可执行映像中包含所有这些结构。


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

由于我有一个非常稳定的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次运行的相对运行时间。

enter image description here

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

长话短说:如果您有足够的能力像下面所做的那样插入enum类型,我可能会推荐它,如果您需要执行RTTI并且性能至关重要。它只需要设置一次成员(确保通过所有构造函数获取它),并且一定不要在以后再编写它。

就是说,这样做不会搞乱您的OOP做法。.仅在类型信息根本不可用并且您发现自己陷入使用RTTI的困境时才使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#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
"
, 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
"
, 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 ) ;
}

标准方式:

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

标准RTTI昂贵,因为它依赖于进行基础字符串比较,因此RTTI的速度可能会因类名长度而异。

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

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

不能保证能正常工作(永远不会产生假阳性,但可能会产生假阴性),但可以快15倍。这依赖于typeid()的实现以某种方式工作,并且您所做的只是比较内部char指针。有时也等同于:

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

但是,您可以安全地使用混合动力,如果类型匹配,混合动力将非常快,对于不匹配的类型,混合动力将是最坏的情况:

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


最好总是进行测量。在下面的代码中,在g ++下,使用手工编码的类型标识似乎比RTTI快三倍。我敢肯定,使用字符串而不是char的更现实的手工编码实现会更慢,从而使时间紧迫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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;
}


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

您也可以通过不使用dynamic_cast来减少开销,而可以通过&typeid(...)==&typeid(type)显式检查类型。尽管这不一定适用于.dll或其他动态加载的代码,但对于静态链接的事物而言,它可能会非常快。

尽管那时候就像使用switch语句,所以就可以了。


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


So, just how expensive is RTTI?

这完全取决于您使用的编译器。我知道有些使用字符串比较,而另一些使用实际算法。

您唯一的希望是编写一个示例程序,然后查看编译器的工作(或至少确定执行一百万个dynamic_casts或一百万个typeid所花费的时间)。


RTTI可以很便宜,并且不需要strcmp。
编译器将测试限制为以相反的顺序执行实际的层次结构。
因此,如果您有一个类C,它是类B的子级,又是类A的子级,则从A * ptr到C * ptr的dynamic_cast意味着只有一个指针比较而不是两个指针(顺便说一句,只有vptr表指针是比较)。测试就像"如果(vptr_of_obj == vptr_of_C)返回(C *)obj"

另一个例子,如果我们尝试从A *到B *进行dynamic_cast。在这种情况下,编译器将依次检查这两种情况(obj为C,而obj为B)。这也可以简化为单个测试(大多数情况下),因为虚拟函数表是作为汇总生成的,因此测试恢复为" if(off(offset_of(vptr_of_obj,B)== vptr_of_B)")

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

的内存布局

1
vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

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

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

例如,这不会编译:

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

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

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

RTTI也是昂贵的,因为它可以使子类层次结构变得模糊(即使有的话)。从"面向对象的编程"中删除"面向对象的"可能会有副作用。