Насколько дорогим является RTTI?
Я понимаю, что с использованием RTTI существует ресурс, но насколько он большой? Везде, где я смотрел, просто говорится, что "RTTI дорогой", но ни один из них не дает никаких контрольных показателей или количественных данных, регулирующих память, время процессора или скорость.
Итак, насколько дорогим является RTTI? Я могу использовать его во встроенной системе, где у меня всего 4 МБ ОЗУ, поэтому каждый бит подсчитывается.
Изменить: В соответствии с ответом С. Лотта, было бы лучше, если бы я включил то, что я на самом деле делаю. Я использую класс для передачи данных различной длины и которые могут выполнять разные действия, поэтому было бы трудно сделать это, используя только виртуальные функции. Кажется, что использование нескольких dynamic_cast
могло бы устранить эту проблему, разрешив передавать разные производные классы через разные уровни, но все же позволить им действовать совершенно по-другому.
С моей точки зрения, dynamic_cast
использует RTTI, поэтому мне было интересно, насколько это возможно было бы использовать в ограниченной системе.
Ответы
Ответ 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 также дорого, потому что он может скрывать иерархию подкласса (если таковой даже есть). Он может иметь побочный эффект удаления "объектно-ориентированного" из "объектно-ориентированного программирования".