当前位置:  开发笔记 > 编程语言 > 正文

RTTI有多贵?

如何解决《RTTI有多贵?》经验,为你挑选了7个好方法。

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

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

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

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



1> 小智..:

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

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

代替

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

前者只涉及一个比较std::type_info; 后者必然涉及遍历继承树和比较.

过去......就像每个人都说的那样,资源使用是特定于实现的.

我同意其他人的意见,提交者应出于设计原因避免使用RTTI.然而,使用RTTI(主要是因为升压::任何的)很好的理由.请注意,了解常见实现中的实际资源使用情况很有用.

我最近在GCC中对RTTI做了大量研究.

tl;博士: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] Known as the Itanium C++ ABI, documented at http://www.codesourcery.com/public/cxx-abi/abi.html. The names are horribly confusing: the name refers to the original development architecture, though the ABI specification works on lots of architectures including i686/x86_64. Comments in GCC's internal source and STL code refer to Itanium as the "new" ABI in contrast to the "old" one they used before. Worse, the "new"/Itanium ABI refers to all versions available through -fabi-version; the "old" ABI predated this versioning. GCC adopted the Itanium/versioned/"new" ABI in version 3.0; the "old" ABI was used in 2.95 and earlier, if I am reading their changelogs correctly.

[2]我无法std::type_info通过平台找到任何资源列表对象的稳定性.对于我有权访问的编译器,我使用了以下内容:echo "#include " | 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对象总是稳定的.


typeid(a)== typeid(b)与B*ba = dynamic_cast (&a)不同.尝试将具有多重继承的对象作为派生类树上的随机级别,您会发现typeid()== typeid()不会产生正数.dynamic_cast是搜索继承树真实的唯一方法.通过禁用RTTI并使用它来停止考虑潜在的节省.如果您的容量过大,那么优化您的代码膨胀.尽量避免在内部循环或任何其他性能关键代码中使用dynamic_cast,你会没事的.
没有RTTI的例外工作.(你被允许扔一个`int`,那里没有vtable :))
异常处理机制必须能够使用满足一些基本要求的任何类型.您可以自由地建议如何在没有RTTI的情况下跨模块边界处理抛出*和捕获*任意类型的异常.请考虑需要向上和向下铸造.
@Deduplicator:然而,当我在编译器中关闭RTTI时,它们工作正常.抱歉让你失望.
@BillyONeal:当你禁用RTTI时,只有部分内容被禁用.具体来说,抛出的所有内容及其适用的基类(递归)仍然可以生成RTTI.
@mcoder这就是为什么文章明确指出"后者必然涉及遍历继承树加上比较".@CoryB当你不需要支持从整个继承树中进行转换时,你可以"负担得起".例如,如果要在集合中查找X类型的所有项目,而不是从X派生的那些项目,那么您应该使用的是前者.如果您还需要查找所有派生实例,则必须使用后者.

2> Izhaki..:

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

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

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()比快速快x20 dyncamic_cast.

图表

在此输入图像描述

代码

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

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


这个基准测试**完全是伪造的优化**:typeid检查是循环不变的,并且被移出循环.它完全没有意义,它是一个基本的基准测试禁忌.
@Kuba:然后基准是虚假的.这并不是优化基准的理由; 这是编写更好基准的原因.
再一次,这是一个失败."对于具有优化的简单转换案例,typeid()比dyncamic_cast快了近20倍." 他们不做同样的事情.dynamic_cast速度较慢是有原因的.

3> Eclipse..:

这取决于事物的规模.在大多数情况下,它只是几个检查和几个指针解引用.在大多数实现中,在具有虚函数的每个对象的顶部,存在指向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(d);
    type_info *ti = vt->typeinfo;
    const char *name = ProcessRawName(ti->name);       
}

一般来说,反对RTTI的真正论据是每次添加新派生类时都必须修改代码的不可维护性.而不是在任何地方使用switch语句,将它们分解为虚函数.这会将类之间不同的所有代码移动到类本身中,这样新的派生只需覆盖所有虚函数即可成为一个功能齐全的类.如果您每次有人检查类的类型并执行不同的操作时,您曾经不得不通过大型代码库进行搜索,那么您将很快学会远离这种编程风格.

如果你的编译器让你完全关闭RTTI,那么在这么小的RAM空间下,最终节省的代码大小可能会很大.编译器需要为每个具有虚函数的类生成一个type_info结构.如果关闭RTTI,则不需要将所有这些结构都包含在可执行映像中.


这个答案是对C++强大功能的低级理解."一般"和"在大多数实现中"使用自由意味着你没有考虑如何很好地使用语言功能.虚拟功能和重新实现RTTI不是解决方案.RTTI就是答案.有时您只想知道对象是否是某种类型.这就是为什么它在那里!所以你丢失了一些KB的RAM到某些type_info结构.啧啧......
+1实际解释为什么使用RTTI被认为是一个糟糕的设计决定,这在我之前并不十分清楚.

4> bobobobo..:

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

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

事实证明,RTTI 昂贵,比同等的昂贵if语句或一个简单的switch在C++一个原始变量.所以美国洛特的答案并不完全正确,还有就是对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 
#include 
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 &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( an ) )
        dogs++;
      else if( dynamic_cast( an ) )
        bigcats++;
      else if( dynamic_cast( an ) )
        cats++;
      else //if( dynamic_cast( 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 &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 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 ) ;
}



5> Marius..:

标准方式:

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 <>或reinterpret_cast < >

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


“标准RTTI昂贵,因为它依赖于进行底层字符串比较”-不,关于此,没有任何“标准”;只是您的实现的typeid :: operator的work_的方式。例如,受支持平台上的GCC已经使用了char *的比较,而无需我们强制-https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01094_source.html。当然,您的方式使MSVC的行为比平台上的默认行为要好得多,因此赞誉不明,我不知道本地使用指针的“某些目标”是什么...但是我的意思是MSVC的行为绝不是“标准”。

6> MSN..:

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

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

虽然在那一点上它就像使用switch语句,所以你去吧.


进入反汇编的dynamic_cast或typeid().operator == for MSVC,你将在那里点击一个strcmp.我假设它存在于一个可怕的情况,你正在与另一个.dll中编译的类型进行比较.它使用了受损的名称,所以至少在相同的编译器下它是正确的.

7> 小智..:

衡量事物总是最好的.在下面的代码中,在g ++下,手动编码类型识别的使用似乎比RTTI快三倍.我确信使用字符串而不是字符的更逼真的手动编码实现会更慢,从而使时间更接近.

#include 
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  ( bp ) ) {
            n++;
        }
#else
        if ( bp->Type() == 'A' ) {
            A * a = static_cast (bp);
            n++;
        }
#endif
    }
    cout << n << endl;
}


它做了一件不同的事情:它还检查bp是否指向从A派生的类型.你的=='A'检查它是否完全指向'A'.我也认为测试有些不公平:编译器可以很容易地看到bp不能指向与A不同的任何东西,但我认为它不会在这里进行优化.
推荐阅读
U友50081205_653
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有