Наскільки дорого коштує RTTI?


152

Я розумію, що від використання RTTI є ресурсне звернення, але наскільки він великий? Всюди, де я дивився, просто сказано, що "RTTI коштує дорого", але жодна з них насправді не дає жодних орієнтирів або кількісних даних, що регулюють пам'ять, час процесора або швидкість.

Отже, наскільки дорого коштує RTTI? Я можу використовувати його у вбудованій системі, де у мене є лише 4 Мб оперативної пам’яті, тому кожен біт рахується.

Редагувати: Відповідно до відповіді С. Лотта , було б краще, якби я включив те, що насправді роблю. Я використовую клас для передачі даних різної довжини, які можуть виконувати різні дії , тому було б важко зробити це, використовуючи лише віртуальні функції. Здається, що використання кількох dynamic_casts може виправити цю проблему, дозволяючи проходити різні похідні класи через різні рівні, але все ж дозволяти їм діяти абсолютно по-різному.

З мого розуміння, dynamic_castвикористовує RTTI, тому мені було цікаво, наскільки це можливо використовувати в обмеженій системі.


1
Як випливає з вашої редакції - дуже часто, коли я виявляю кілька динамічних кастингів, я розумію, що за допомогою шаблону Visitor вирівнюються речі знову. Це могло б працювати для вас?
фільтрований

4
Я викладу це так: я щойно почав використовувати dynamic_castв C ++, і тепер, 9 з 10 разів, коли я "ламаю" програму налагоджувачем, вона порушується всередині внутрішньої функції динамічного відтворення. Чорт повільно.
користувач541686

3
RTTI = "До речі, інформація про тип часу".
Номенон

Відповіді:


115

Незалежно від компілятора, ви завжди можете заощадити на час виконання, якщо можете дозволити це зробити

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 (головним чином через boost :: any). Зважаючи на це, корисно знати його фактичне використання ресурсів у загальних реалізаціях.

Нещодавно я провів ряд досліджень RTTI в GCC.

tl; dr: RTTI в GCC використовує незначний простір і 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)насправді просто порівнює внутрішнє покажчик.

У кращому ABI GCC клас vtable завжди містить вказівник на структуру RTTI на тип, хоча він може не використовуватися. Таким чином, typeid()сам виклик повинен коштувати стільки, скільки будь-який інший Vtable Lookup (такий же, як виклик функції віртуального члена), а підтримка RTTI не повинна використовувати додаткового місця для кожного об'єкта.

З того, що я можу зробити, структури RTTI, використовувані GCC (це всі підкласи std::type_info), містять лише кілька байтів для кожного типу, окрім назви. Мені не зрозуміло, чи є імена у вихідному коді навіть із -fno-rtti. Так чи інакше, зміна розміру складеного двійкового файлу має відображати зміну використання пам’яті під час виконання.

Швидкий експеримент (з використанням GCC 4.4.3 на 64-розрядному Ubuntu 10.04) показує, що -fno-rttiнасправді збільшується двійковий розмір простої програми тестування на кілька сотень байт. Це відбувається послідовно в комбінаціях -gта -O3. Я не впевнений, чому розмір збільшився б; одна можливість полягає в тому, що STL-код GCC поводиться по-різному без 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 : "старий" ABI передував цій версії. GCC прийняв Itanium / verioned / "новий" ABI у версії 3.0; "старий" ABI використовувався в 2,95 і раніше, якщо я читаю їхні журнали змін.

[2] Не вдалося знайти жодного ресурсу, що перераховує std::type_infoстабільність об'єкта за платформою. Для компіляторів , я мав доступ до, я використовував наступне: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Цей макрос керує поведінкою operator==для std::type_infoSTCC GCC, ніж GCC 3.0. Я виявив, що mingw32-gcc підкоряється Windows C ++ ABI, де std::type_infoоб'єкти не унікальні для типу через DLL; typeid(a) == typeid(b)дзвінки strcmpпід кришками. Я припускаю, що на однопрограмних вбудованих цілях, таких як AVR, де немає коду, з яким можна зв’язатись, std::type_infoоб'єкти завжди стабільні.


6
Винятки працюють без RTTI. (Вам дозволяється кидати, intі в цьому немає жодної
версії

3
@Deduplicator: І все ж, коли я вимикаю RTTI у своєму компіляторі, вони працюють добре. Вибачте, що розчарували.
Біллі ONeal

5
Механізм поводження з винятками повинен бути здатний працювати з будь-яким типом, повним заповненням декількох основних вимог. Ви можете запропонувати, як керувати викидами та ловом винятків довільного типу через межі модулів без RTTI. Будь ласка, врахуйте, що необхідне лиття вгору та вниз.
Дедуплікатор

15
typeid (a) == typeid (b) НЕ такий самий, як B * ba = dynamic_cast <B *> (& a). Спробуйте це на об'єктах із множинним успадкуванням як випадковий рівень на похідному дереві класів, і ви знайдете typeid () == typeid () не дасть позитиву. Динамічний_каст - це єдиний спосіб пошуку справжнього дерева спадкування. Перестаньте думати про потенційну економію, відключивши RTTI та просто використовуйте його. Якщо у вас є надмірна потужність, тоді оптимізуйте код. Постарайтеся уникати використання динамичного_cast всередині внутрішніх циклів або будь-якого іншого критичного коду щодо ефективності, і вам буде добре.
mysticcoder

3
@mcoder Тому стаття прямо говорить про це the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Ви можете "дозволити" це зробити, коли вам не потрібно підтримувати кастинг із усього дерева спадкування. Наприклад, якщо ви хочете знайти всі колекції типу X у колекції, але не ті, що походять від X, то ви повинні використовувати колишнє. Якщо вам також потрібно знайти всі похідні екземпляри, вам доведеться використовувати останні.
Айдіакапі

48

Можливо, ці цифри допоможуть.

Я робив швидкий тест, використовуючи це:

  • GCC Clock () + XCode's Profiler.
  • 100 000 000 ітерацій циклу.
  • Двоядерний Intel Xeon 2 x 2,66 ГГц.
  • Розглянутий клас походить від одного базового класу.
  • 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 <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;
}

1
Звичайно, динамічний акторський склад є загальнішим - він працює, якщо елемент виведений більш виведено. Наприклад, class a {}; class b : public a {}; class c : public b {};коли ціль є екземпляром, він cбуде добре працювати при тестуванні для класу bз dynamic_cast, але не з typeidрішенням. Але все-таки розумно, +1
Біллі ONeal

34
Цей орієнтир є неправдивим з оптимізаціями : типова перевірка не є інваріантною циклом і переміщується з циклу. Це зовсім не цікаво, це базовий бенчмаркінг ні-ні.
Відновіть Моніку

3
@Kuba: Тоді тест є хибним. Це не привід для порівняння з вимкненими оптимізаціями; це привід писати кращі орієнтири.
Біллі ONeal

3
знову ж таки, це невдача. "Для простих випадків кастингу з оптимізацією typeid () майже на x20 швидше, ніж dyncamic_cast." вони НЕ роблять те ж саме. Існує причина, що у динамічних передач повільніше.
містикодер

1
@KubaOber: всього +1. це так класично. і з вигляду числа циклів повинно бути очевидно, що це сталося.
v.oddou

38

Це залежить від масштабу речей. Здебільшого це всього лише кілька перевірок і кілька перенаправлень вказівників. У більшості реалізацій у верхній частині кожного об'єкта, який має віртуальні функції, є вказівник на 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 є неможливість необхідності змінювати код скрізь кожного разу, коли ви додаєте новий похідний клас. Замість того, щоб переключати оператори скрізь, розподіліть їх на віртуальні функції. Це переміщує весь код, який відрізняється між класами, до самих класів, так що новому виведенню просто потрібно перекрити всі віртуальні функції, щоб стати повноцінно функціонуючим класом. Якщо вам коли-небудь доводилося шукати велику базу коду кожного разу, коли хтось перевіряє тип класу і робить щось інше, ви швидко навчитесь триматися осторонь цього стилю програмування.

Якщо ваш компілятор дозволяє повністю вимкнути RTTI, остаточна економія розміру коду може бути значною, хоча при такому невеликому просторі оперативної пам'яті. Компілятору необхідно створити структуру type_info для кожного класу з віртуальною функцією. Якщо вимкнути RTTI, всі ці структури не потрібно включати у виконуваний образ.


4
+1 за те, що насправді пояснює, чому використання RTTI вважається поганим дизайнерським рішенням, мені раніше це було не зовсім зрозуміло.
aguazales

6
Ця відповідь є низьким рівнем розуміння сили С ++. "Взагалі" та "У більшості реалізацій", що використовуються в основному, означає, що ви не замислюєтеся про те, як добре використовувати функції мови. Віртуальні функції та повторна реалізація RTTI - це не відповідь. RTTI - це відповідь. Іноді просто хочеться дізнатися, чи є об’єкт певного типу. Ось чому це там! Таким чином, ви втрачаєте кілька КБ оперативної пам’яті на деякі структури type_info. Гей ...
mysticcoder

16

Ну а профайлер ніколи не бреше.

Оскільки у мене досить стабільна ієрархія 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 <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 ) ;
}

13

Стандартний спосіб:

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;

Щоб зрозуміти, чи потрібно це оптимізувати, вам потрібно побачити, скільки часу ви витрачаєте на отримання нового пакета, порівняно з часом, який потрібно на обробку пакету. У більшості випадків порівняння рядків, ймовірно, не буде великим накладним. (залежно від вашого класу чи простору імен :: довжина імені класу)

Найбезпечніший спосіб оптимізувати це - реалізувати власний typeid як int (або enum Type: int) як частину вашого базового класу та використовувати його для визначення типу класу, а потім просто використовувати static_cast <> або reinterpret_cast < >

Для мене різниця приблизно в 15 разів на неоптимізовану MS VS 2005 C ++ SP1.


2
"Стандартний RTTI є дорогим, оскільки він покладається на те, щоб виконати базове порівняння рядків" - ні, в цьому немає нічого "Стандартного"; це просто , як ваша реалізація - х typeid::operatorроботи з . Наприклад, GCC на підтримуваній платформі вже використовує порівняння char *s, не примушуючи нас до цього - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Звичайно, ваш спосіб змушує MSVC вести себе набагато краще, ніж за замовчуванням на вашій платформі, тому кудо, і я не знаю, що таке "деякі цілі", які в основному використовують вказівники ... але моя думка полягає в тому, що поведінка MSVC ні в якому разі "Стандарт".
підкреслити_d

7

Для простої перевірки, RTTI може бути таким же дешевим, як і порівняння вказівника. Для перевірки спадщини це може бути таким же дорогим, як і strcmpдля кожного типу дерева спадкування, якщо ви єdynamic_cast переходите від верху до низу однією реалізацією там.

Ви також можете зменшити накладні витрати, не використовуючи dynamic_cast та замість цього чітко перевіряючи тип за допомогою & typeid (...) == & typeid (type). Хоча це не обов'язково працює для .dlls або іншого динамічно завантаженого коду, це може бути досить швидко для речей, які є статично пов'язаними.

Хоча в цей момент це все одно, що використовувати оператор переключення, так що ви йдете.


1
Чи є у вас будь-які посилання на версію strcmp? Вкрай неефективним і неточним є використання strcmp для перевірки типу.
JaredPar

У поганій реалізації, яка може мати декілька об'єктів type_info на тип, вона може реалізувати bool type_info :: operator == (const type_info & x) const як "! Strcmp (name (), x.name ())"
Грег Роджерс,

3
Вступайте в розбирання або динамичного_cast, або typeid (). Оператор == для MSVC, і ви натиснете strcmp туди. Я припускаю його там для жахливого випадку, коли ви порівнюєте з типом, складеним в іншому .dll. І він використовує кепське ім'я, так що принаймні це правильно, дається тому ж компілятору.
MSN

1
ви повинні робити "typeid (...) == typeid (type)", а не порівнювати адресу
Йоханнес Шауб - ліб

1
Моя думка, що ви можете робити & typeid (...) == & typeid (бла), як рано, і будете в безпеці. Насправді це не може зробити нічого корисного, оскільки typeid (...) може генеруватися на стеці, але якщо їх адреси рівні, то їхні типи рівні.
MSN

6

Завжди найкраще вимірювати речі. У наступному коді під 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;
}

1
намагайтеся робити це не з динамічним_cast, а з typeid. це може прискорити продуктивність.
Йоханнес Шауб - ліб

1
але використання динамичного_cast є більш реалістичним, принаймні, дивлячись на мій код

2
це робить інакше: він також перевіряє, чи bp вказує на тип, похідний від A., ваш == 'A' перевіряє, чи точно він вказує на 'A'. Я також думаю, що тест дещо несправедливий: компілятор може легко побачити, що bp не може вказувати на щось інше, ніж A., але я думаю, що тут не оптимізується.
Йоханнес Шауб - ліб

все одно я перевірив ваш код. і це дає мені "0,016s" для RTTI і "0,044s" для віртуальних викликів функції. (з використанням -O2)
Йоханнес Шауб - ліб

хоча зміна його на використання typeid тут не має ніякої різниці (все ще 0.016s)
Йоханнес Шауб - litb

4

Нещодавно я вимірював часові витрати на RTTI у конкретних випадках MSVC та GCC для 3GHz PowerPC. У тестах я працював (досить великий додаток C ++ з глибоким деревом класу), кожен dynamic_cast<>коштував від 0,8 мкс до 2 мкс, залежно від того, потрапив він чи пропустив.


2

Отже, наскільки дорого коштує RTTI?

Це повністю залежить від компілятора, який ви використовуєте. Я розумію, що одні використовують рядкові порівняння, а інші використовують реальні алгоритми.

Ваша єдина надія - написати прикладну програму і подивитися, що робить ваш компілятор (або принаймні визначити, скільки часу потрібно на виконання мільйона dynamic_castsабо мільйона typeidс).


1

RTTI може бути дешевим і не потребує stcmp. Компілятор обмежує тест на виконання фактичної ієрархії у зворотному порядку. Отже, якщо у вас є клас C, який є дочірнім класом B, який є дочіркою класу A, динамічний_cast від A * ptr до C * ptr означає лише порівняння одного вказівника, а не два (BTW, лише вказівник таблиці vptr є порівняно). Тест виглядає як "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Ще один приклад, якщо ми спробуємо динамічно передати з 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 ]

Як компілятор знає, щоб оптимізувати це під час компіляції?

Під час компіляції компілятор знає поточну ієрархію об'єктів, тому відмовляється компілювати різні типи ієрархії з динамічним випуском. Тоді потрібно просто обробити глибину ієрархії та додати обернену кількість тестів, щоб відповідати такій глибині.

Наприклад, це не компілюється:

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

-5

RTTI може бути «дорогим», оскільки ви додаєте виписку if якщо ви робите порівняння RTTI. У глибоко вкладених ітераціях це може бути дорого. У чомусь, що ніколи не виконується в циклі, це по суті безкоштовно.

Вибір полягає у використанні належного поліморфного дизайну, виключаючи твердження if. У глибоко вкладених петлях це важливо для виконання. Інакше це не має великого значення.

RTTI також дорогий тим, що може затьмарити ієрархію підкласів (якщо така є навіть). Це може мати побічний ефект від видалення "об'єктно-орієнтованого" з "об'єктно-орієнтованого програмування".


2
Не обов’язково - я збирався використовувати це опосередковано через динамический_cast, і підтримувати ієрархію на місці, тому що мені потрібно скинути, оскільки кожен підтип повинен мати різні (змінного розміру) дані, які потрібно застосовувати по-різному, отже, динамічний_каст.
Крістіан Ромо,

1
@ Крістіан Ромо: Будь ласка, оновіть своє запитання новими фактами. dynam_cast - це (іноді) необхідне зло в C ++. Питання про продуктивність RTTI, коли вас змушують це робити, не має великого сенсу.
S.Lott

@ S.Lott: Оновлено. Вибачте за плутанину.
Крістіан Ромо,

1
Я зробив експеримент з цього приводу саме зараз - виявляється, RTTI значно дорожче, ніж ifзаява, яку ви вводите, коли ви перевіряєте інформацію про час виконання таким чином.
бобобобо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.