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_cast
s 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_cast
korzysta z RTTI, więc zastanawiałem się, jak wykonalne byłoby użycie w ograniczonym systemie.
źródło
dynamic_cast
w 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.Odpowiedzi:
Niezależnie od kompilatora, zawsze możesz zaoszczędzić na czasie wykonywania, jeśli możesz sobie na to pozwolić
zamiast
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 są 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śnytypeid(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-rtti
rzeczywistości zwiększa się rozmiar binarny prostego programu testowego o kilkaset bajtów. Dzieje się to konsekwentnie w przypadku kombinacji-g
i-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_info
stabilnoś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 zachowanieoperator==
forstd::type_info
w STL GCC, począwszy od GCC 3.0. Zauważyłem, że mingw32-gcc jest zgodny z systemem Windows C ++ ABI, gdziestd::type_info
obiekty nie są unikalne dla typu w bibliotekach DLL;typeid(a) == typeid(b)
rozmowystrcmp
pod 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_info
obiekty są zawsze stabilne.źródło
int
i nie ma w tymthe 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.Być może te liczby pomogą.
Robiłem szybki test używając tego:
Przetestowano 5 przypadków:
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):
Zatem wniosek byłby taki:
typeid()
jest ponad dwa razy szybszy niżdyncamic_cast
.Z optymalizacją (-Os)
Zatem wniosek byłby taki:
typeid()
jest prawie x20 szybszy niżdyncamic_cast
.Wykres
Kod
Zgodnie z prośbą w komentarzach, kod jest poniżej (trochę niechlujny, ale działa). „FastDelegate.h” jest dostępny tutaj .
źródło
class a {}; class b : public a {}; class c : public b {};
Gdy cel jest instancją programuc
będzie działał dobrze podczas testowania dla klasyb
z rozwiązaniemdynamic_cast
, ale nie ztypeid
rozwiązaniem. Wciąż rozsądne, +1To 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 ++:
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.
źródło
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ż tylko
if
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
if
oświadczenie lub prostaswitch
na 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 posiadaniaif
oś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ń.
Zgadza się,
dynamicCasts
zajęło to 94% czasu działania. NatomiastregularSwitch
blok zajął tylko 3,3% .Krótko mówiąc: jeśli możesz sobie pozwolić na energię, by podłączyć się
enum
i 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.
źródło
Standardowy sposób:
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ć:
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:
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:
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.
źródło
typeid::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".Dla prostego sprawdzenia RTTI może być tak tani jak porównanie wskaźnikowe. W przypadku sprawdzania dziedziczenia może być tak kosztowne jak
strcmp
dla każdego typu w drzewie dziedziczenia, jeśli przechodziszdynamic_cast
od góry do dołu w jednej implementacji.Możesz również zmniejszyć narzut, nie używając
dynamic_cast
i 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.
źródło
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.
źródło
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.źródło
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_casts
lub milionatypeid
sekund).źródło
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
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:
źródło
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”.
źródło
if
oświadczenia przedstawisz gdy zaznaczysz wykonania informacja typu w ten sposób.