web-dev-qa-db-ja.com

RTTIはどれくらい高価ですか?

RTTIを使用するとリソースヒットが発生することは理解していますが、どれくらいの大きさですか?私が見たところどこでも、「RTTIは高価」とだけ言っていますが、実際にベンチマーク、またはメモリ、プロセッサ時間、または速度を保護する定量的なデータを提供するものはありません。

それでは、RTTIはどれだけ高価なのでしょうか? RAMが4MBしかない組み込みシステムで使用する場合があるため、すべてのビットがカウントされます。

編集: S。Lottの回答による 、実際にやっていることを含めるとよいでしょう。 クラスを使用して異なる長さのデータを渡し、異なるアクションを実行できます したがって、仮想関数のみを使用してこれを行うことは困難です。いくつかのdynamic_castsは、異なる派生クラスが異なるレベルを通過できるようにすることでこの問題を解決できますが、それらは完全に異なる動作を許可します。

私の理解から、dynamic_castはRTTIを使用しているため、限られたシステムでどのように使用するのが実現可能か疑問に思っていました。

144
Cristián Romo

コンパイラに関係なく、実行する余裕がある場合はいつでもランタイムを節約できます

_if (typeid(a) == typeid(b)) {
  B* ba = static_cast<B*>(&a);
  etc;
}
_

の代わりに

_B* ba = dynamic_cast<B*>(&a);
if (ba) {
  etc;
}
_

前者は_std::type_info_の1つの比較のみを含みます。後者は必然的に継承ツリーの走査と比較を伴います。

それを過ぎて...誰もが言うように、リソースの使用は実装に固有です。

設計上の理由から、提出者はRTTIを避けるべきだという他の皆のコメントに同意します。ただし、RTTIを使用する正当な理由があります(主にboost :: anyのため)。それを念頭に置いて、一般的な実装での実際のリソース使用量を知ることは有用です。

私は最近、GCCでRTTIについて多くの研究を行いました。

tl; dr:GCCのRTTIはごくわずかなスペースを使用し、typeid(a) == typeid(b)は非常に高速で、多くのプラットフォーム(Linux、BSD、おそらくはmbwではなく組み込みプラットフォーム)で使用されます。常に恵まれたプラットフォームにいることを知っているなら、RTTIは無料に近いです。

ざらざらした詳細:

GCCは特定の「ベンダー中立」C++ ABI [1]を使用することを好み、常にこのABIをLinuxおよびBSDターゲットに使用します[2]。このABIと弱いリンケージをサポートするプラットフォームの場合、typeid()は、動的リンクの境界を越えても、各タイプに対して一貫した一意のオブジェクトを返します。 &typeid(a) == &typeid(b)をテストするか、移植可能なテストtypeid(a) == typeid(b)が実際に内部的にポインターを比較するだけであるという事実に依存することができます。

GCCの推奨ABIでは、クラスvtablealwaysは、タイプごとのRTTI構造体へのポインタを保持しますが、使用されない場合があります。したがって、typeid()呼び出し自体shouldは、他のvtableルックアップ(仮想メンバー関数の呼び出しと同じ)だけのコストで、RTTIサポート各オブジェクトに余分なスペースを使用しないでください

私が理解できることから、GCC(これらはすべて_std::type_info_のサブクラスです)が使用するRTTI構造体は、名前を除いて、各タイプに対して数バイトしか保持していません。 _-fno-rtti_を使用しても、出力コードに名前が存在するかどうかはわかりません。いずれにしても、コンパイルされたバイナリのサイズの変更は、ランタイムメモリ使用量の変更を反映する必要があります。

簡単な実験(Ubuntu 10.04 64ビットでGCC 4.4.3を使用)は、_-fno-rtti_が実際に増加する単純なテストプログラムのバイナリサイズを数百バイト増やすことを示しています。これは、_-g_と_-O3_の組み合わせで一貫して発生します。サイズが大きくなる理由がわかりません。 1つの可能性は、GCCのSTLコードがRTTIなしで異なる動作をすることです(例外が機能しないため)。

[1] Itanium C++ ABIとして知られ、 http://www.codesourcery.com/public/cxx-abi/abi.html で文書化されています。 ABI仕様はi686/x86_64を含む多くのアーキテクチャで動作しますが、名前は恐ろしく紛らわしいです。名前は元の開発アーキテクチャを指します。 GCCの内部ソースおよびSTLコードのコメントでは、Itaniumを以前使用していた「古い」ものとは対照的に「新しい」ABIと呼んでいます。さらに悪いことに、「新しい」/ Itanium ABIは_-fabi-version_で利用可能なallバージョンを指します。 「古い」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==_に対する_std::type_info_の動作を制御します。 mingw32-gccはWindows C++ ABIに準拠していることがわかりました。ここで_std::type_info_オブジェクトはDLL全体の型に対して一意ではありません。 typeid(a) == typeid(b)は、strcmpを呼び出します。リンクするコードがないAVRのような単一プログラムの組み込みターゲットでは、_std::type_info_オブジェクトは常に安定していると推測します。

113
sbrudenell

おそらく、これらの数字が役立つでしょう。

私はこれを使用して簡単なテストを行っていました:

  • GCC Clock()+ XCodeのプロファイラー。
  • 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_より2倍以上高速です。
  • 最新のマシンでは、この2つの差は約1ナノ秒(100万分の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

コード

コメントで要求されているように、コードは下にあります(少し複雑ですが、動作します)。 '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;
}
_
45
Izhaki

それは物事の規模に依存します。ほとんどの場合、それはほんの数回のチェックといくつかのポインターの逆参照です。ほとんどの実装では、仮想関数を持つすべてのオブジェクトの上部に、そのクラスの仮想関数のすべての実装へのポインターのリストを保持する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スペース。仮想関数:RTTIをオフにした場合、これらのすべての構造を実行可能イメージに含める必要はありません。

37
Eclipse

さて、プロファイラーは決して嘘をつきません。

私は18-20タイプの非常に安定した階層を持っているので、あまり変わらないので、単純なenum'dメンバーを使用するだけでトリックを行い、意図的に回避するかどうか疑問に思いましたRTTIの「高」コスト。 RTTIが実際に導入するifステートメントよりも高価である場合、私は懐疑的でした。少年よ、少年だ。

RTTI is高価、はるかに同等のifステートメントまたはC++のプリミティブ変数の単純なswitchよりも高価であることがわかりました。したがって、S.Lottの答えは完全に正しいわけではなく、RTTIには追加コストがあり、notはちょうどミックスにifステートメントがある RTTIが非常に高価だからです。

このテストは、Apple LLVM 5.0コンパイラで実行され、ストック最適化がオンになっています(デフォルトのリリースモード設定)。

そのため、以下の2つの関数があり、それぞれ1)RTTIまたは2)単純なスイッチのいずれかを介してオブジェクトの具体的なタイプを計算します。これは50,000,000回行われます。さらに苦労せずに、50,000,000回の実行の相対ランタイムを紹介します。

enter image description here

そうです、dynamicCastsはランタイムの94%を取りました。 regularSwitchブロックは.3%のみを取りましたが。

簡単に言えば、次のようにenum 'd型をフックするエネルギーを確保できるなら、RTTIandパフォーマンスが最重要です。メンバーを設定するだけでonce(必ずall constructorsを介して取得してください)、その後は決して書き込まないでください。

とはいえ、これを行うとOOP practice ..を台無しにしないでください。 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 ) ;
}
16
bobobobo

標準的な方法:

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;

canただし、タイプが一致する場合は非常に高速で、一致しないタイプの場合は最悪になるハイブリッドを安全に使用します。

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

これを最適化する必要があるかどうかを理解するには、パケットの処理にかかる時間と比較して、新しいパケットの取得に費やす時間を確認する必要があります。ほとんどの場合、文字列の比較はおそらく大きなオーバーヘッドにはなりません。 (クラスまたは名前空間::クラス名の長さに依存)

これを最適化する最も安全な方法は、Baseクラスの一部として独自のtypeidをint(またはenum Type:int)として実装し、それを使用してクラスの型を決定し、次にstatic_cast <>またはreinterpret_cast <を使用することです>

私にとって、違いは最適化されていないMS VS 2005 C++ SP1で約15倍です。

13
Marius

単純なチェックの場合、RTTIはポインター比較と同じくらい安価です。継承チェックの場合、ある実装で上から下にdynamic_cast--実行している場合、継承ツリーのすべてのタイプのstrcmpと同じくらい高価になる可能性があります。

dynamic_castを使用せず、代わりに&typeid(...)==&typeid(type)を使用して明示的に型を確認することにより、オーバーヘッドを削減することもできます。これは、.dllやその他の動的にロードされるコードでは必ずしも機能しませんが、静的にリンクされているものでは非常に高速です。

その時点では、switchステートメントを使用するようなものです。

6
MSN

常に物事を測定することが最善です。次のコードでは、g ++では、手動でコード化された型識別の使用は、RTTIの約3倍高速であるようです。文字の代わりに文字列を使用した、より現実的な手書きコードの実装は、タイミングが近くなり、遅くなると確信しています。

#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;
}
6
anon

しばらく前に、3GHz PowerPCのMSVCとGCCの特定のケースでRTTIの時間コストを測定しました。私が実行したテスト(深いクラスツリーを持つかなり大きなC++アプリ)で、それぞれdynamic_cast<>コストは、ヒットしたかミスしたかに応じて、0.8μs〜2μsの間です。

4
Crashworks

それでは、RTTIはどれだけ高価なのでしょうか?

それは使用しているコンパイラに完全に依存します。文字列比較を使用するものと、実際のアルゴリズムを使用するものがあることを理解しています。

あなたの唯一の望みは、サンプルプログラムを書いて、あなたのコンパイラが何をするかを見ることです(または、少なくとも百万を実行するのにかかる時間を決定するdynamic_castsまたは100万typeids)。

2
Max Lybbert

RTTIは安価であり、必ずしもstrcmpを必要としません。コンパイラは、実際の階層を逆順で実行するようにテストを制限します。したがって、クラスAの子であるクラスBの子であるクラスCがある場合、A * ptrからC * ptrへのdynamic_castは、2つではなく1つのポインター比較のみを意味します(BTW、vptrテーブルポインターのみが比較)。テストは「if(vptr_of_obj == vptr_of_C)return(C *)obj」のようなものです

別の例として、dynamic * castをA *からB *にしようとした場合。その場合、コンパイラは両方のケース(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);  
1
X-Ryl669