Насколько дорого стоит RTTI?

Я понимаю, что использование RTTI снижает ресурс, но насколько он велик? Куда бы я ни посмотрел, везде написано, что «RTTI - дорогое удовольствие», но ни один из них на самом деле не дает никаких тестов или количественных данных, касающихся памяти, времени процессора или скорости.

Итак, насколько дорого стоит RTTI? Я мог бы использовать его во встроенной системе, где у меня всего 4 МБ ОЗУ, поэтому каждый бит на счету.

Изменить: Согласно ответу С. Лотта , было бы лучше, если бы я включил то, что я на самом деле делаю. Я использую класс для передачи данных разной длины и который может выполнять разные действия , поэтому было бы сложно сделать это, используя только виртуальные функции. Кажется, что использование нескольких dynamic_cast s может решить эту проблему, позволяя различным производным классам проходить через разные уровни, но при этом позволяя им действовать совершенно по-разному.

dynamic_cast Насколько я понимаю, использует RTTI, поэтому мне было интересно, насколько возможно его использовать в ограниченной системе.

Ответов (11)

Независимо от компилятора, вы всегда можете сэкономить на времени выполнения, если можете себе позволить

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 ( в основном из - за повышения :: любой). Имея это в виду, полезно знать фактическое использование ресурсов в распространенных реализациях.

Недавно я провел несколько исследований 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 (так же, как вызов виртуальной функции-члена), а поддержка 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 / versioned / "новый" 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== for std::type_info в STL GCC, начиная с GCC 3.0. Я обнаружил, что mingw32-gcc подчиняется Windows C++ ABI, где std::type_info объекты не уникальны для типа в библиотеках DLL; typeid(a) == typeid(b) звонит strcmp под одеяло. Я предполагаю, что в однопрограммных встроенных целях, таких как AVR, где нет кода для связывания, std::type_info объекты всегда стабильны.

Возможно, эти цифры помогут.

Я делал быстрый тест, используя это:

  • GCC Clock () + Профайлер XCode.
  • 100000000 итераций цикла.
  • 2 двухъядерных процессора Intel Xeon с тактовой частотой 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()почти в 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;
}

Что ж, профайлер никогда не врет.

Поскольку у меня есть довольно стабильная иерархия из 18-20 типов, которая не очень сильно меняется, я задавался вопросом, поможет ли просто использование простого члена enum'd трюк и избежать якобы «высокой» стоимости RTTI. Я скептически относился к тому, что RTTI на самом деле дороже, чем просто if вводимое в нем заявление. Мальчик, о, мальчик, не так ли?

Оказывается, RTTI стоит дорого, намного дороже, чем эквивалентный if оператор или простой switch оператор для примитивной переменной в C++. Так ответ С. Лотт является не совсем корректно, то есть дополнительные расходы на RTTI, и это не из - за просто имея ifзаявление в миксе. Это связано с тем, что RTTI стоит очень дорого.

Этот тест проводился на компиляторе Apple LLVM 5.0 с включенной стандартной оптимизацией (настройки режима выпуска по умолчанию).

Итак, у меня есть две функции, каждая из которых определяет конкретный тип объекта через 1) RTTI или 2) простой переключатель. Так происходит 50 000 000 раз. Без лишних слов, я представляю вам относительное время выполнения для 50 000 000 запусков.

введите описание изображения здесь

Правильно, на это dynamicCasts ушло 94% времени выполнения. Пока regularSwitch блок занял всего 3,3% .

Короче говоря: если вы можете позволить себе энергию для подключения и enum типа d, как я сделал ниже, я, вероятно, порекомендовал бы это, если вам нужно сделать RTTI, а производительность имеет первостепенное значение. Требуется установить член только один раз (убедитесь, что вы получили его через все конструкторы ), и никогда не записывайте его позже.

Тем не менее, выполнение этого не должно испортить вашу практику ООП ... оно предназначено для использования только тогда, когда информация о типе просто недоступна, и вы обнаруживаете, что загнаны в угол, чтобы использовать 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 ) ;
}

Всегда лучше измерять вещи. В следующем коде под 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;
}

Это зависит от масштаба вещей. По большей части это всего лишь пара проверок и несколько разыменований указателей. В большинстве реализаций в верхней части каждого объекта, который имеет виртуальные функции, есть указатель на 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, окончательная экономия размера кода может быть значительной при таком небольшом пространстве ОЗУ. Компилятору необходимо сгенерировать структуру type_info для каждого отдельного класса с виртуальной функцией. Если вы отключите RTTI, все эти структуры не нужно будет включать в исполняемый образ.

RTTI может быть «дорогостоящим», потому что вы добавляете оператор if каждый раз, когда выполняете сравнение RTTI. В глубоко вложенных итерациях это может быть дорого. В том, что никогда не выполняется в цикле, это, по сути, бесплатно.

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

RTTI также является дорогостоящим, поскольку может скрыть иерархию подклассов (если она вообще есть). Побочным эффектом этого может быть удаление «объектно-ориентированного» из «объектно-ориентированного программирования».

Для простой проверки RTTI может быть столь же дешевым, как сравнение указателей. Для проверки наследования это может быть столь же затратно, как strcmp для каждого типа в дереве наследования, если вы выполняете dynamic_cast сверху вниз в одной реализации.

Вы также можете уменьшить накладные расходы, не используя dynamic_cast и вместо этого явно проверяя тип с помощью & typeid (...) == & typeid (type). Хотя это не обязательно работает для .dll или другого динамически загружаемого кода, это может быть довольно быстро для вещей, которые статически связаны.

Хотя в этот момент это похоже на использование оператора switch, так что готово.

Итак, насколько дорого стоит RTTI?

Это полностью зависит от используемого вами компилятора. Я понимаю, что некоторые используют сравнения строк, а другие используют реальные алгоритмы.

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

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

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) как часть вашего базового класса и использовать его для определения типа класса, а затем просто использовать static_cast <> или reinterpret_cast < >

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

RTTI может быть дешевым и не обязательно требует strcmp. Компилятор ограничивает тест выполнением фактической иерархии в обратном порядке. Итак, если у вас есть класс C, который является дочерним элементом класса B, который является дочерним элементом класса A, dynamic_cast от A * ptr до C * ptr подразумевает только одно сравнение указателей, а не два (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 = вернуть 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);  

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