Насколько дорогим является RTTI? - программирование

Насколько дорогим является RTTI?

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

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

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

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

4b9b3361

Ответ 1

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

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 предпочитает использовать конкретный "нейтральный поставщик" С++ ABI [1] и всегда использует эти цели ABI для Linux и BSD [2]. Для платформ, поддерживающих этот ABI, а также слабую связь, typeid() возвращает согласованный и уникальный объект для каждого типа, даже для динамических границ привязки. Вы можете протестировать &typeid(a) == &typeid(b) или просто полагаться на то, что переносимый тест typeid(a) == typeid(b) действительно просто сравнивает указатель внутри.

В предпочтительном ABC ABC класс vtable всегда содержит указатель на структуру RTTI для каждого типа, хотя он может и не использоваться. Таким образом, вызов typeid() должен стоить столько же, сколько любой другой просмотр vtable (так же, как вызов функции виртуального участника), а поддержка RTTI не должна использовать дополнительное пространство для каждого объекта.

Из того, что я могу разобрать, структуры RTTI, используемые GCC (это все подклассы std::type_info), содержат только несколько байтов для каждого типа, кроме имени. Мне непонятно, существуют ли имена в выходном коде даже с -fno-rtti. В любом случае изменение размера скомпилированного двоичного файла должно отражать изменение использования памяти во время выполнения.

Быстрый эксперимент (с использованием GCC 4.4.3 на Ubuntu 10.04 64-bit) показывает, что -fno-rtti фактически увеличивает двоичный размер простой тестовой программы на несколько сотен байт. Это происходит последовательно между комбинациями -g и -O3. Я не уверен, почему размер увеличился бы; одна из возможностей заключается в том, что код GCC STL ведет себя по-разному без RTTI (поскольку исключения не будут работать).

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

Ответ 2

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

Например, в псевдо-С++:

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, все эти структуры не обязательно должны быть включены в исполняемый образ.

Ответ 3

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

Я быстро проверил это:

  • Провайдер GCC Clock() + XCode.
  • 100 000 000 итераций цикла.
  • 2 x 2,66 ГГц двухъядерный 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.
  • На современной машине разница между двумя составляет около 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.

Диаграмма

enter image description here

Код

Как указано в комментариях, код ниже (немного грязный, но работает). "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;
}

Ответ 4

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

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

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

Ответ 5

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

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

Оказывается, что RTTI дорогой, намного больше дороже, чем эквивалентный оператор if или простой switch для примитивной переменной в С++. Поэтому ответ S.Lott не совсем корректен, для RTTI есть дополнительные затраты, и это связано не только с наличием оператора if в миксе. Это связано с тем, что RTTI очень дорогой.

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

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

enter image description here

Правильно, 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 |= 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 ) ;
}

Ответ 6

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

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

Хотя в этот момент ему нравится использовать оператор switch, поэтому вы идете.

Ответ 7

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

Ответ 8

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

Ответ 9

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

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

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

Ответ 10

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 = 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);  

Ответ 11

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

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

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