Ile kosztuje RTTI?

152

Rozumiem, że użycie RTTI ma wpływ na zasoby, ale jak duże jest to? Wszędzie, gdzie spojrzałem, mówi się tylko, że „RTTI jest drogie”, ale żaden z nich nie podaje żadnych testów porównawczych ani danych ilościowych dotyczących pamięci, czasu procesora lub szybkości.

Więc jak drogie jest RTTI? Mógłbym go użyć w systemie wbudowanym, w którym mam tylko 4 MB pamięci RAM, więc liczy się każdy bit.

Edycja: zgodnie z odpowiedzią S. Lotta byłoby lepiej, gdybym zawarł to, co faktycznie robię. Używam klasy do przekazywania danych o różnej długości i która może wykonywać różne czynności , więc byłoby to trudne przy użyciu tylko funkcji wirtualnych. Wydaje się, że użycie kilku dynamic_casts może rozwiązać ten problem, pozwalając różnym klasom pochodnym na przechodzenie przez różne poziomy, ale nadal pozwalają im działać zupełnie inaczej.

Z mojego zrozumienia dynamic_castkorzysta z RTTI, więc zastanawiałem się, jak wykonalne byłoby użycie w ograniczonym systemie.

Cristián Romo
źródło
1
Idąc za twoją edycją - bardzo często, gdy robię kilka dynamicznych rzutów, zdaję sobie sprawę, że użycie wzorca Visitor ponownie poprawia sytuację. Czy to może Ci pomóc?
philsquared
4
Ujmę to w ten sposób - właśnie zacząłem używać dynamic_castw C ++, a teraz, 9 na 10 razy, kiedy "zepsuję" program za pomocą debuggera, psuje się on wewnątrz wewnętrznej funkcji dynamicznego rzutowania. Jest cholernie wolno.
user541686
3
Nawiasem mówiąc, RTTI = "informacje o typie czasu wykonywania".
Noumenon

Odpowiedzi:

115

Niezależnie od kompilatora, zawsze możesz zaoszczędzić na czasie wykonywania, jeśli możesz sobie na to pozwolić

if (typeid(a) == typeid(b)) {
  B* ba = static_cast<B*>(&a);
  etc;
}

zamiast

B* ba = dynamic_cast<B*>(&a);
if (ba) {
  etc;
}

Pierwsza obejmuje tylko jedno porównanie std::type_info; ta ostatnia wymaga koniecznie przejścia przez drzewo dziedziczenia oraz porównań.

Poza tym ... jak wszyscy mówią, wykorzystanie zasobów zależy od implementacji.

Zgadzam się z komentarzami wszystkich innych, że zgłaszający powinien unikać RTTI ze względów projektowych. Istnieje jednak powody, aby skorzystać RTTI (głównie z powodu boost :: dowolnym). Mając to na uwadze, warto znać rzeczywiste wykorzystanie zasobów w typowych implementacjach.

Niedawno przeprowadziłem kilka badań nad RTTI w GCC.

tl; dr: RTTI w GCC zajmuje niewiele miejsca i typeid(a) == typeid(b)jest bardzo szybki, na wielu platformach (Linux, BSD i być może platformy wbudowane, ale nie mingw32). Jeśli wiesz, że zawsze będziesz na błogosławionej platformie, RTTI jest bardzo blisko darmowego.

Ziarniste szczegóły:

GCC woli używać określonego "niezależnego od dostawcy" C ++ ABI [1] i zawsze używa tego ABI dla celów Linux i BSD [2]. W przypadku platform obsługujących ten interfejs ABI, a także słabe powiązanie, typeid()zwraca spójny i niepowtarzalny obiekt dla każdego typu, nawet w przypadku granic dynamicznego łączenia. Możesz testować &typeid(a) == &typeid(b)lub po prostu polegać na fakcie, że test przenośny typeid(a) == typeid(b)faktycznie porównuje wewnętrznie wskaźnik.

W preferowanym ABI GCC klasa vtable zawsze zawiera wskaźnik do struktury RTTI dla danego typu, chociaż może nie być używana. Zatem typeid()samo wywołanie powinno kosztować tyle samo, co każde inne wyszukiwanie w tabeli vtable (tak samo jak wywołanie wirtualnej funkcji składowej), a obsługa RTTI nie powinna wykorzystywać dodatkowej przestrzeni dla każdego obiektu.

Z tego, co widzę, struktury RTTI używane przez GCC (to wszystkie podklasy std::type_info) mają tylko kilka bajtów dla każdego typu, poza nazwą. Nie jest dla mnie jasne, czy nazwy są obecne w kodzie wyjściowym nawet z -fno-rtti. Tak czy inaczej, zmiana rozmiaru skompilowanego pliku binarnego powinna odzwierciedlać zmianę użycia pamięci w czasie wykonywania.

Szybki eksperyment (z użyciem GCC 4.4.3 na Ubuntu 10.04 64-bit) pokazuje, że w -fno-rttirzeczywistości zwiększa się rozmiar binarny prostego programu testowego o kilkaset bajtów. Dzieje się to konsekwentnie w przypadku kombinacji -gi -O3. Nie jestem pewien, dlaczego rozmiar miałby się zwiększyć; jedną z możliwości jest to, że kod STL GCC zachowuje się inaczej bez RTTI (ponieważ wyjątki nie będą działać).

[1] Znany jako Itanium C ++ ABI, udokumentowany pod adresem http://www.codesourcery.com/public/cxx-abi/abi.html . Nazwy są strasznie zagmatwane: nazwa odnosi się do oryginalnej architektury programistycznej, chociaż specyfikacja ABI działa na wielu architekturach, w tym i686 / x86_64. Komentarze w wewnętrznym źródle GCC i kodzie STL odnoszą się do Itanium jako „nowego” ABI w przeciwieństwie do „starego”, którego używali wcześniej. Co gorsza, „nowy” / Itanium ABI odnosi się do wszystkich wersji dostępnych za pośrednictwem -fabi-version; „stary” ABI poprzedził tę wersję. GCC przyjęło Itanium / versioned / "nowy" ABI w wersji 3.0; "stary" ABI był używany w 2.95 i wcześniejszych, jeśli dobrze czytam ich dzienniki zmian.

[2] Nie mogłem znaleźć żadnego zasobu zawierającego listę std::type_infostabilności obiektów według platformy. Dla kompilatorów miałem dostęp do użyłem co następuje: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. To makro kontroluje zachowanie operator==for std::type_infow STL GCC, począwszy od GCC 3.0. Zauważyłem, że mingw32-gcc jest zgodny z systemem Windows C ++ ABI, gdzie std::type_infoobiekty nie są unikalne dla typu w bibliotekach DLL; typeid(a) == typeid(b)rozmowy strcmppod kołdrą. Spekuluję, że w przypadku celów osadzonych w jednym programie, takich jak AVR, gdzie nie ma kodu, z którym można by się połączyć, std::type_infoobiekty są zawsze stabilne.

sbrudenell
źródło
6
Wyjątki działają bez RTTI. (Możesz rzucić inti nie ma w tym
tabeli
3
@Deduplicator: A jednak, kiedy wyłączam RTTI w moim kompilatorze, działają dobrze. Przepraszam że cię zawiodłem.
Billy ONeal
5
Mechanizm obsługi wyjątków musi być w stanie działać z każdym typem spełniającym kilka podstawowych wymagań. Możesz zasugerować, jak radzić sobie z wyrzucaniem i przechwytywaniem wyjątków dowolnego typu poza granicami modułu bez RTTI. Proszę wziąć pod uwagę, że wymagane jest rzucanie w górę iw dół.
Deduplicator
15
typeid (a) == typeid (b) NIE jest tym samym, co B * ba = dynamic_cast <B *> (& a). Wypróbuj to na obiektach z wielokrotnym dziedziczeniem jako losowym poziomie w pochodnym drzewie klas, a zobaczysz, że typeid () == typeid () nie da wyniku dodatniego. dynamic_cast to jedyny sposób na przeszukiwanie drzewa dziedziczenia. Przestań myśleć o potencjalnych oszczędnościach, wyłączając RTTI i po prostu z niego korzystaj. Jeśli masz zbyt dużą pojemność, zoptymalizuj nadmiar kodu. Staraj się unikać używania dynamic_cast wewnątrz wewnętrznych pętli lub jakiegokolwiek innego kodu krytycznego dla wydajności, a wszystko będzie dobrze.
mysticcoder
3
@mcoder Dlatego artykuł wyraźnie to stwierdza the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB „Możesz sobie na to pozwolić”, gdy nie potrzebujesz obsługi rzutowania z całego drzewa dziedziczenia. Na przykład, jeśli chcesz znaleźć wszystkie elementy typu X w kolekcji, ale nie te, które pochodzą z X, to powinieneś użyć tego pierwszego. Jeśli chcesz również znaleźć wszystkie instancje pochodne, musisz użyć tego drugiego.
Aidiakapi,
48

Być może te liczby pomogą.

Robiłem szybki test używając tego:

  • Zegar GCC () + Profiler XCode.
  • 100 000 000 iteracji pętli.
  • Dwurdzeniowy procesor Intel Xeon 2 x 2,66 GHz.
  • Omawiana klasa pochodzi z jednej klasy bazowej.
  • typeid (). name () zwraca „N12fastdelegate13FastDelegate1IivEE”

Przetestowano 5 przypadków:

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 to po prostu mój rzeczywisty kod, ponieważ musiałem utworzyć obiekt tego typu przed sprawdzeniem, czy jest podobny do tego, który już mam.

Bez optymalizacji

Dla których wyniki były (uśredniłem kilka przebiegów):

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.

Zatem wniosek byłby taki:

  • W przypadku prostych przypadków rzutowania bez optymalizacji typeid()jest ponad dwa razy szybszy niż dyncamic_cast.
  • Na nowoczesnej maszynie różnica między nimi wynosi około 1 nanosekundy (milionowej części milisekundy).

Z optymalizacją (-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.

Zatem wniosek byłby taki:

  • W przypadku prostych przypadków rzutowania z optymalizacją typeid()jest prawie x20 szybszy niż dyncamic_cast.

Wykres

wprowadź opis obrazu tutaj

Kod

Zgodnie z prośbą w komentarzach, kod jest poniżej (trochę niechlujny, ale działa). „FastDelegate.h” jest dostępny tutaj .

#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;
}
Izhaki
źródło
1
Oczywiście rzut dynamiczny jest bardziej ogólny - działa, jeśli element jest bardziej pochodny. Np. class a {}; class b : public a {}; class c : public b {};Gdy cel jest instancją programu cbędzie działał dobrze podczas testowania dla klasy bz rozwiązaniem dynamic_cast, ale nie z typeidrozwiązaniem. Wciąż rozsądne, +1
Billy ONeal
34
Ten wzorzec jest całkowicie fałszywy z optymalizacjami : sprawdzanie identyfikatora typu jest niezmienne w pętli i jest usuwane z pętli. To wcale nie jest interesujące, to podstawowy test porównawczy, nie, nie.
Przywróć Monikę
3
@Kuba: Zatem benchmark jest fałszywy. To nie jest powód do testów porównawczych z wyłączonymi optymalizacjami; to powód, aby pisać lepsze testy porównawcze.
Billy ONeal
3
po raz kolejny jest to porażka. „W przypadku prostych przypadków rzutowania z optymalizacją typeid () jest prawie x20 szybszy niż dyncamic_cast”. NIE robią tego samego. Jest powód, dla którego dynamic_cast jest wolniejszy.
mysticcoder
1
@KubaOber: łącznie +1. to jest takie klasyczne. i z wyglądu liczby cykli powinno jasno wynikać, że tak się stało.
v.oddou,
38

To zależy od skali rzeczy. W większości to tylko kilka sprawdzeń i kilka wyłuskiwań wskaźnika. W większości implementacji na górze każdego obiektu, który ma funkcje wirtualne, znajduje się wskaźnik do tabeli vtable, która zawiera listę wskaźników do wszystkich implementacji funkcji wirtualnej w tej klasie. Domyślam się, że większość implementacji użyłaby tego do przechowywania innego wskaźnika do struktury type_info dla klasy.

Na przykład w 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);       
}

Ogólnie rzecz biorąc, prawdziwym argumentem przeciwko RTTI jest niemożność konserwacji polegająca na konieczności modyfikowania kodu wszędzie za każdym razem, gdy dodajesz nową klasę pochodną. Zamiast wszędzie umieszczać instrukcje przełącznika, uwzględnij je w funkcjach wirtualnych. Spowoduje to przeniesienie całego kodu, który jest różny między klasami, do samych klas, tak więc nowe pochodzenie musi po prostu przesłonić wszystkie funkcje wirtualne, aby stać się w pełni funkcjonującą klasą. Jeśli kiedykolwiek musiałeś przeszukiwać dużą bazę kodu za każdym razem, gdy ktoś sprawdza typ klasy i robi coś innego, szybko nauczysz się trzymać z daleka od tego stylu programowania.

Jeśli Twój kompilator pozwala całkowicie wyłączyć RTTI, ostateczne oszczędności rozmiaru kodu mogą być jednak znaczące przy tak małej przestrzeni RAM. Kompilator musi wygenerować strukturę type_info dla każdej klasy z funkcją wirtualną. Jeśli wyłączysz RTTI, wszystkie te struktury nie muszą być zawarte w obrazie wykonywalnym.

Zaćmienie
źródło
4
+1 za wyjaśnienie, dlaczego użycie RTTI jest uważane za złą decyzję projektową, co wcześniej nie było dla mnie całkiem jasne.
aguazales,
6
Ta odpowiedź to niski poziom zrozumienia potęgi C ++. „Ogólnie” i „W większości implementacji” używane swobodnie oznaczają, że nie myślisz o tym, jak dobrze korzystać z funkcji języków. Funkcje wirtualne i ponowne wdrożenie RTTI nie są odpowiedzią. RTTI jest odpowiedzią. Czasami po prostu chcesz wiedzieć, czy obiekt jest określonego typu. Dlatego tam jest! Więc tracisz kilka KB pamięci RAM na rzecz niektórych struktur type_info. Ojej ...
mysticcoder,
16

Cóż, profiler nigdy nie kłamie.

Ponieważ mam dość stabilną hierarchię 18-20 typów, która nie zmienia się zbytnio, zastanawiałem się, czy samo użycie prostego elementu wyliczeniowego załatwi sprawę i pozwoli uniknąć rzekomo „wysokiego” kosztu RTTI. Byłem sceptyczny, czy RTTI jest rzeczywiście droższe niż tylkoif oświadczenie, które wprowadza. O chłopcze, czy to jest to.

Okazuje się, że RTTI jest drogie, znacznie droższe niż równoważne ifoświadczenie lub prosta switchna prymitywnej zmiennej w C ++. Tak więc odpowiedź S. Lotta nie jest całkowicie poprawna, RTTI wiąże się z dodatkowymi kosztami i nie wynika to tylko z posiadania ifoświadczenia . To dlatego, że RTTI jest bardzo drogie.

Ten test został wykonany na kompilatorze Apple LLVM 5.0 z włączoną optymalizacją zapasów (domyślne ustawienia trybu wydania).

Mam więc poniżej 2 funkcje, z których każda określa konkretny typ obiektu za pomocą 1) RTTI lub 2) prostego przełącznika. Robi to 50 000 000 razy. Bez zbędnych ceregieli przedstawiam względne czasy działania dla 50 000 000 uruchomień.

wprowadź opis obrazu tutaj

Zgadza się, dynamicCastszajęło to 94% czasu działania. Natomiast regularSwitchblok zajął tylko 3,3% .

Krótko mówiąc: jeśli możesz sobie pozwolić na energię, by podłączyć się enumi pisać, tak jak to zrobiłem poniżej, prawdopodobnie poleciłbym to, jeśli potrzebujesz RTTI, a wydajność jest najważniejsza. Wystarczy ustawić członka tylko raz (upewnij się, że otrzymujesz go za pośrednictwem wszystkich konstruktorów ) i nigdy nie zapisz tego później.

To powiedziawszy, zrobienie tego nie powinno zepsuć twoich praktyk OOP .. jest przeznaczone do użycia tylko wtedy, gdy informacje o typie po prostu nie są dostępne i jesteś zmuszony do korzystania z 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 ) ;
}
bobobobo
źródło
13

Standardowy sposób:

cout << (typeid(Base) == typeid(Derived)) << endl;

Standardowe RTTI jest drogie, ponieważ opiera się na porównywaniu łańcuchów bazowych, a zatem szybkość RTTI może się różnić w zależności od długości nazwy klasy.

Powodem, dla którego używane są porównania ciągów, jest zapewnienie spójnego działania w granicach bibliotek / DLL. Jeśli budujesz swoją aplikację statycznie i / lub używasz pewnych kompilatorów, prawdopodobnie możesz użyć:

cout << (typeid(Base).name() == typeid(Derived).name()) << endl;

Nie ma gwarancji, że zadziała (nigdy nie da wyniku fałszywie dodatniego, ale może dać wynik fałszywie ujemny), ale może być nawet 15 razy szybszy. Zależy to od implementacji typeid () do działania w określony sposób, a jedyne, co robisz, to porównywanie wewnętrznego wskaźnika znaku. Czasami jest to równoważne z:

cout << (&typeid(Base) == &typeid(Derived)) << endl;

Państwo może jednak używać bezpiecznie hybrydę, która będzie bardzo szybko jeśli rodzaje dopasować i będzie najgorszym przypadku niedopasowanych typów:

cout << ( typeid(Base).name() == typeid(Derived).name() || 
          typeid(Base) == typeid(Derived) ) << endl;

Aby zrozumieć, czy musisz to zoptymalizować, musisz sprawdzić, ile czasu spędzasz na odbieraniu nowego pakietu w porównaniu z czasem potrzebnym na przetworzenie pakietu. W większości przypadków porównanie ciągów prawdopodobnie nie będzie dużym narzutem. (w zależności od klasy lub przestrzeni nazw :: długość nazwy klasy)

Najbezpieczniejszym sposobem na zoptymalizowanie tego jest zaimplementowanie własnego identyfikatora typu jako int (lub wyliczenia Type: int) jako części klasy bazowej i użycie go do określenia typu klasy, a następnie po prostu użyj static_cast <> lub reinterpret_cast < >

Dla mnie różnica jest około 15-krotna na niezoptymalizowanym MS VS 2005 C ++ SP1.

Marius
źródło
2
„Standardowe RTTI jest drogie, ponieważ polega na przeprowadzaniu podstawowego porównania ciągów” - nie, nie ma w tym nic „Standardowego”; tak właśnie działa Twoja implementacjatypeid::operator . Na przykład GCC na obsługiwanej platformie już korzysta z porównań char *s, bez naszego wymuszania - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Jasne, twój sposób sprawia, że ​​MSVC zachowuje się dużo lepiej niż domyślne na twojej platformie, więc chwała, i nie wiem, jakie są "niektóre cele", które natywnie używają wskaźników ... ale moim celem jest to, że zachowanie MSVC nie jest w żaden sposób "Standard".
underscore_d
7

Dla prostego sprawdzenia RTTI może być tak tani jak porównanie wskaźnikowe. W przypadku sprawdzania dziedziczenia może być tak kosztowne jak strcmpdla każdego typu w drzewie dziedziczenia, jeśli przechodzisz dynamic_castod góry do dołu w jednej implementacji.

Możesz również zmniejszyć narzut, nie używając dynamic_casti zamiast tego jawnie sprawdzając typ za pomocą & typeid (...) == & typeid (type). Chociaż niekoniecznie działa to w przypadku plików .dll lub innego dynamicznie ładowanego kodu, może być dość szybkie w przypadku rzeczy, które są statycznie połączone.

Chociaż w tym momencie jest to jak użycie instrukcji switch, więc gotowe.

MSN
źródło
1
Czy masz jakieś referencje do wersji strcmp? Używanie strcmp do sprawdzania typu wydaje się wyjątkowo nieefektywne i niedokładne.
JaredPar
W kiepskiej implementacji, która może mieć wiele obiektów type_info na typ, może zaimplementować bool type_info :: operator == (const type_info & x) const as "! Strcmp (name (), x.name ())"
Greg Rogers
3
Wkrocz do deasemblacji operatora dynamic_cast lub typeid (). == dla MSVC, a trafisz tam strcmp. Zakładam, że jest tam dla okropnego przypadku, w którym porównujesz z typem skompilowanym w innym pliku dll. I używa zniekształconej nazwy, więc przynajmniej jest poprawna, biorąc pod uwagę ten sam kompilator.
MSN
1
powinieneś zrobić "typeid (...) == typeid (type)" i nie porównywać adresu
Johannes Schaub - litb
1
Chodzi mi o to, że możesz zrobić & typeid (...) == & typeid (bla) jak najwcześniej i będzie to bezpieczne. Może faktycznie nie przynieść nic pożytecznego, ponieważ typeid (...) mógłby zostać wygenerowany na stosie, ale jeśli ich adresy są równe, to ich typy są równe.
MSN
6

Zawsze najlepiej jest mierzyć rzeczy. W poniższym kodzie, pod g ++, użycie ręcznej identyfikacji typu wydaje się być około trzy razy szybsze niż RTTI. Jestem pewien, że bardziej realistyczna implementacja kodowana ręcznie przy użyciu łańcuchów zamiast znaków byłaby wolniejsza, zbliżając czasy.

#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;
}

źródło
1
staraj się nie robić tego z dynamic_cast, ale z typeid. może przyspieszyć działanie.
Johannes Schaub - litb
1
ale stosując dynamic_cast jest bardziej realistyczny, przynajmniej patrząc na mojego kodu
2
robi co innego: sprawdza również, czy bp wskazuje na typ pochodzący z A. your == 'A' sprawdza, czy wskazuje dokładnie na 'A'. Myślę też, że test jest nieco niesprawiedliwy: kompilator z łatwością widzi, że bp nie może wskazywać niczego innego niż A., ale myślę, że tutaj nie optymalizuje.
Johannes Schaub - litb
w każdym razie przetestowałem Twój kod. i daje mi "0,016s" dla RTTI i "0,044s" dla wywołań funkcji wirtualnych. (przy -O2)
Johannes Schaub - litb
choć zmiana na typeid nie robi tu żadnej różnicy (nadal 0,016s)
Johannes Schaub - litb
4

Jakiś czas temu zmierzyłem koszty czasu RTTI w konkretnych przypadkach MSVC i GCC dla PowerPC 3 GHz. W testach, które przeprowadziłem (dość duża aplikacja C ++ z głębokim drzewem klas), każdy dynamic_cast<>kosztował od 0,8 μs do 2 μs, w zależności od tego, czy trafił, czy nie.

Crashworks
źródło
2

Więc jak drogie jest RTTI?

To zależy całkowicie od używanego kompilatora. Rozumiem, że niektórzy używają porównań ciągów, a inni używają prawdziwych algorytmów.

Jedyną nadzieją jest napisanie przykładowego programu i zobaczenie, co robi Twój kompilator (lub przynajmniej określenie, ile czasu zajmie wykonanie miliona dynamic_castslub miliona typeidsekund).

Max Lybbert
źródło
1

RTTI może być tani i niekoniecznie potrzebuje strcmp. Kompilator ogranicza test do wykonania rzeczywistej hierarchii w odwrotnej kolejności. Więc jeśli masz klasę C, która jest dzieckiem klasy B, która jest dzieckiem klasy A, dynamic_cast z A * ptr do C * ptr implikuje tylko jedno porównanie wskaźników, a nie dwa (BTW, tylko wskaźnik tabeli vptr jest w porównaniu). Test jest podobny do "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Inny przykład, jeśli spróbujemy dynamicznie przesyłać z A * do B *. W takim przypadku kompilator sprawdzi po kolei oba przypadki (obj to C, a obj to B). Można to również uprościć do pojedynczego testu (w większości przypadków), ponieważ tabela funkcji wirtualnych jest tworzona jako agregacja, więc test zostanie wznowiony do „if (offset_of (vptr_of_obj, B) == vptr_of_B)” z

offset_of = return sizeof (vptr_table)> = sizeof (vptr_of_B)? vptr_of_new_methods_in_B: 0

Układ pamięci

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

Skąd kompilator wie o optymalizacji tego w czasie kompilacji?

W czasie kompilacji kompilator zna aktualną hierarchię obiektów, więc odmawia kompilowania dynamicznego_casting innej hierarchii typów. Następnie musi tylko obsłużyć głębokość hierarchii i dodać odwróconą liczbę testów, aby dopasować taką głębokość.

Na przykład to się nie kompiluje:

void * something = [...]; 
// Compile time error: Can't convert from something to MyClass, no hierarchy relation
MyClass * c = dynamic_cast<MyClass*>(something);  
X-Ryl669
źródło
-5

RTTI może być „drogie”, ponieważ za każdym razem, gdy robisz porównanie RTTI, dodajesz instrukcję if. W mocno zagnieżdżonych iteracjach może to być kosztowne. W czymś, co nigdy nie jest wykonywane w pętli, jest zasadniczo bezpłatne.

Wybór polega na zastosowaniu odpowiedniego projektu polimorficznego, eliminując instrukcję if. W przypadku głęboko zagnieżdżonych pętli ma to zasadnicze znaczenie dla wydajności. W przeciwnym razie nie ma to większego znaczenia.

RTTI jest również kosztowne, ponieważ może zaciemniać hierarchię podklas (jeśli w ogóle istnieje). Skutkiem ubocznym może być usunięcie „programowania obiektowego” z „programowania zorientowanego obiektowo”.

S.Lott
źródło
2
Niekoniecznie - zamierzałem użyć go pośrednio przez dynamic_cast i zachować hierarchię na miejscu, ponieważ muszę obniżyć poziom, ponieważ każdy podtyp musi mieć różne (o zmiennej wielkości) dane, które muszą być stosowane inaczej, stąd dynamic_cast.
Cristián Romo
1
@ Cristián Romo: Proszę zaktualizować swoje pytanie o te nowe fakty. dynamic_cast jest (czasami) złem koniecznym w C ++. Pytanie o wydajność RTTI, gdy jesteś do tego zmuszony, nie ma większego sensu.
S.Lott,
@ S.Lott: Aktualizacja. Przepraszam za zamieszanie.
Cristián Romo
1
Zrobiłem eksperyment o tym właśnie teraz - okazuje się RTTI jest znacznie droższe niż ifoświadczenia przedstawisz gdy zaznaczysz wykonania informacja typu w ten sposób.
bobobobo