167

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

では、RTTIはどれくらいの費用がかかりますか?RAMが4MBしかない組み込みシステムで使用する可能性があるため、すべてのビットが重要になります。

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

私の理解で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;の1つの比較のみを含みます。後者は必然的に継承ツリーと比較をトラバースする必要があります。

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

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

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

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

ざらざらした詳細:

GCCは、特定の「ベンダー中立」C ++ ABI [1]を使用することを好み、LinuxおよびBSDターゲット[2]には常にこのABIを使用します。この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ビットでGCC4.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コードのコメントでは、以前使用していた「古い」ABIとは対照的に、Itaniumを「新しい」ABIと呼んでいます。さらに悪いことに、「新しい」/ Itanium ABIは、 ;を通じて利用可能なすべてのバージョンを指します。-fabi-version「古い」ABIはこのバージョン管理よりも前のものです。GCCはバージョン3.0でItanium/versioned/"new"ABIを採用しました。「古い」ABIは、変更ログを正しく読み取っていれば、2.95以前で使用されていました。

std::type_info[2]プラットフォームごとのリソースリストオブジェクトの安定性が見つかりませんでした。アクセスできるコンパイラーについては、以下を使用しましたecho "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES。このマクロは、GCC 3.0以降、GCCのSTLでのoperator==forの動作を制御します。std::type_infomingw32-gccはWindowsC++ ABIに準拠していることがわかりました。この場合、std::type_infoオブジェクトはDLL全体のタイプに対して一意ではありません。typeid(a) == typeid(b)隠れて電話strcmpします。リンクするコードがないAVRのような単一プログラムの組み込みターゲットでは、std::type_infoオブジェクトは常に安定していると思います。

于 2010-12-02T11:25:59.817 に答える
55

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

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

  • 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()は、 よりも 2 倍以上高速ですdyncamic_cast
  • 最新のマシンでは、この 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()は よりも 20 倍近く高速です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

それは物事の規模に依存します。ほとんどの場合、それはほんの2、3のチェックといくつかのポインターの逆参照です。ほとんどの実装では、仮想関数を持つすべてのオブジェクトの上部に、そのクラスの仮想関数のすべての実装へのポインターのリストを保持する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<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 の「高い」コストを回避できるのではないかと考えました。if私は、RTTI が実際にそれが導入する声明よりも高価であったかどうか懐疑的でした. 少年ああ、少年ですね。

RTTIは高価であり、同等のステートメントやC++ の単純なプリミティブ変数よりもはるかに高価であることが判明しました。したがって、S.Lott の回答は完全に正しいわけではありません。RTTI に追加のコストがかかります。これはステートメントが混在しているからというだけではありません。これは、RTTI が非常に高価であるためです。ifswitchif

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

したがって、以下の 2 つの関数があり、それぞれが 1) RTTI または 2) 単純なスイッチを介してオブジェクトの具体的なタイプを把握します。それは 50,000,000 回行われます。これ以上苦労することなく、50,000,000 回の実行に対する相対ランタイムを提示します。

ここに画像の説明を入力

そうです、実行時間の94%dynamicCastsがかかりました。ブロックは3.3%しかかかりませんでした。regularSwitch

enum簡単に言うと、RTTI を実行する必要があり、パフォーマンスが最も重要な場合は、以下で行ったように 'd 型をフックするエネルギーがある場合は、おそらくそれをお勧めます。メンバーの設定は1 回だけで済み(必ずすべてのコンストラクターを介して取得してください)、その後は絶対に書き込まないでください。

とは言っても、これを行うことで 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 はポインタ比較と同じくらい安価です。継承チェックの場合、ある実装で上から下に -ing してstrcmpいる場合、継承ツリーのすべてのタイプと同じくらい高価になる可能性があります。dynamic_cast

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

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

于 2009-02-24T00:00:40.927 に答える
6

物事を測定することは常に最善です。次のコードでは、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;
}
于 2009-02-24T15:11:26.917 に答える
4

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

于 2010-12-02T11:32:51.163 に答える
2

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」のようなものです

別の例として、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 に答える
2

では、RTTI はどれくらいの費用がかかるのでしょうか?

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

dynamic_casts唯一の希望は、サンプル プログラムを作成し、コンパイラが何を行うかを確認することです (または、少なくとも 100 万または 100 万の sを実行するのにかかる時間を決定しますtypeid)。

于 2009-02-24T01:12:20.880 に答える
-4

RTTI比較を行うたびにifステートメントを追加しているため、RTTIは「高価」になる可能性があります。深くネストされた反復では、これは高額になる可能性があります。ループで実行されることのないものでは、本質的に無料です。

選択は、適切なポリモーフィックデザインを使用して、ifステートメントを排除することです。深くネストされたループでは、これはパフォーマンスにとって不可欠です。そうでなければ、それはあまり重要ではありません。

RTTIは、サブクラス階層が不明瞭になる可能性があるため(存在する場合でも)、コストもかかります。「オブジェクト指向プログラミング」から「オブジェクト指向」を削除するという副作用があります。

于 2009-02-23T23:56:57.863 に答える