Czy ktoś może szczegółowo wyjaśnić, jak dokładnie działa wirtualna tabela i jakie wskaźniki są powiązane, gdy wywoływane są funkcje wirtualne.
Jeśli faktycznie są wolniejsze, czy możesz pokazać, że czas potrzebny na wykonanie funkcji wirtualnej jest dłuższy niż normalne metody klasowe? Łatwo jest zapomnieć o tym, jak / co się dzieje, nie widząc żadnego kodu.
Odpowiedzi:
Metody wirtualne są zwykle implementowane za pomocą tak zwanych wirtualnych tabel metod (w skrócie vtable), w których przechowywane są wskaźniki funkcji. Dodaje to pośrednie rzeczywiste wywołanie (muszę pobrać adres funkcji do wywołania z vtable, a następnie wywołać ją - w przeciwieństwie do zwykłego wywoływania z wyprzedzeniem). Oczywiście zajmuje to trochę czasu i trochę kodu.
Jednak niekoniecznie jest to główna przyczyna spowolnienia. Prawdziwy problem polega na tym, że kompilator (ogólnie / zwykle) nie może wiedzieć, która funkcja zostanie wywołana. Więc nie może go wstawić ani wykonać żadnych innych takich optymalizacji. To samo może dodać tuzin bezcelowych instrukcji (przygotowanie rejestrów, wywoływanie, a następnie przywrócenie stanu) i może hamować inne, pozornie niezwiązane optymalizacje. Co więcej, jeśli rozgałęziasz się jak szalony, wywołując wiele różnych implementacji, cierpisz z powodu tych samych trafień, które cierpiałbyś z powodu rozgałęziania się jak szalony innymi sposobami: pamięć podręczna i predyktor gałęzi ci nie pomogą, rozgałęzienia potrwają dłużej niż całkowicie przewidywalne Oddział.
Duże, ale : te hity wydajnościowe są zwykle zbyt małe, aby mieć znaczenie. Warto je rozważyć, jeśli chcesz utworzyć kod o wysokiej wydajności i rozważyć dodanie funkcji wirtualnej, która byłaby wywoływana z alarmującą częstotliwością. Jednak również pamiętać, że zastąpienie wywołania funkcji wirtualnych z innymi środkami rozgałęzienia (
if .. else
,switch
, wskaźników funkcji, etc.) nie rozwiązuje podstawowego problemu - może to równie dobrze być wolniejsze. Problemem (jeśli w ogóle istnieje) nie są funkcje wirtualne, ale (niepotrzebna) pośrednictwo.Edycja: Różnica w instrukcjach połączeń jest opisana w innych odpowiedziach. Zasadniczo kod dla wywołania statycznego („normalnego”) to:
Wirtualne wywołanie robi dokładnie to samo, z tym wyjątkiem, że adres funkcji nie jest znany w czasie kompilacji. Zamiast tego kilka instrukcji ...
Jeśli chodzi o gałęzie: gałąź to cokolwiek, co przeskakuje do innej instrukcji zamiast pozwolić na wykonanie następnej instrukcji. Obejmuje to
if
,switch
, części różnych pętli, wywołania funkcji itp a czasami implementuje kompilatora rzeczy, które nie wydają się gałęzi w sposób, który rzeczywiście potrzebuje oddział pod maską. Zobacz Dlaczego przetwarzanie posortowanej tablicy jest szybsze niż nieposortowanej tablicy? dlaczego może to być powolne, co procesory robią, aby przeciwdziałać temu spowolnieniu i jak to nie jest lekarstwem na wszystko.źródło
virtual
.Oto kilka faktycznych zdemontowanych kodów odpowiednio z wirtualnego wywołania funkcji i wywołania innego niż wirtualne:
Możesz zobaczyć, że połączenie wirtualne wymaga trzech dodatkowych instrukcji, aby wyszukać poprawny adres, podczas gdy adres połączenia niewirtualnego może zostać wkompilowany.
Należy jednak pamiętać, że w większości przypadków ten dodatkowy czas wyszukiwania można uznać za nieistotny. W sytuacjach, w których czas wyszukiwania byłby znaczący, np. W pętli, wartość można zwykle buforować, wykonując pierwsze trzy instrukcje przed pętlą.
Inną sytuacją, w której czas wyszukiwania staje się znaczący, jest posiadanie kolekcji obiektów i wykonywanie pętli poprzez wywoływanie funkcji wirtualnej na każdym z nich. Jednakże w tym przypadku, będziesz potrzebować kilka sposobów wybierania, które działają zadzwonić i tak, i wirtualny tabeli odnośników jest równie dobrym sposobem jak każdy inny. W rzeczywistości, ponieważ kod wyszukiwania vtable jest tak szeroko stosowany, że jest mocno zoptymalizowany, więc próba obejścia go ręcznie ma duże szanse na pogorszenie wydajności.
źródło
-0x8(%rbp)
. o mój ... ta składnia AT&T.Wolniej niż co ?
Funkcje wirtualne rozwiązują problem, którego nie można rozwiązać za pomocą bezpośrednich wywołań funkcji. Ogólnie można porównywać tylko dwa programy, które obliczają to samo. „Ten ray tracer jest szybszy niż ten kompilator” nie ma sensu, a ta zasada uogólnia nawet na małe rzeczy, takie jak poszczególne funkcje lub konstrukcje języka programowania.
Jeśli nie używasz funkcji wirtualnej, aby dynamicznie przełączać się na fragment kodu oparty na układzie odniesienia, na przykład typ obiektu, będziesz musiał użyć czegoś innego, na przykład
switch
instrukcji, aby osiągnąć to samo. To, że coś innego ma swoje własne koszty ogólne, a także wpływ na organizację programu, które wpływają na jego utrzymanie i globalne wyniki.Zauważ, że w C ++ wywołania funkcji wirtualnych nie zawsze są dynamiczne. Kiedy wywołania są wykonywane na obiekcie, którego dokładny typ jest znany (ponieważ obiekt nie jest wskaźnikiem ani referencją, lub ponieważ jego typ można inaczej wywnioskować statycznie), wówczas są to zwykłe wywołania funkcji składowych. Oznacza to nie tylko, że nie ma narzutu związanego z wysyłką, ale także, że połączenia te mogą być wprowadzane w taki sam sposób, jak zwykłe połączenia.
Innymi słowy, Twój kompilator C ++ może działać, gdy funkcje wirtualne nie wymagają wirtualnego wysyłania, więc zwykle nie ma powodu, aby martwić się ich wydajnością w porównaniu z funkcjami innymi niż wirtualne.
Nowość: Nie możemy również zapominać o bibliotekach udostępnionych. Jeśli używasz klasy znajdującej się we wspólnej bibliotece, wywołanie zwykłej funkcji składowej nie będzie po prostu ładną sekwencją instrukcji, taką jak
callq 0x4007aa
. Musi przejść przez kilka obręczy, takich jak pośrednie poprzez „tabelę łączy programów” lub jakąś taką strukturę. Dlatego pośrednia biblioteka współdzielona może w pewnym stopniu (jeśli nie całkowicie) zrównoważyć różnicę kosztów między (prawdziwie pośrednim) połączeniem wirtualnym a połączeniem bezpośrednim. Dlatego rozumowanie dotyczące kompromisów funkcji wirtualnych musi uwzględniać budowę programu: czy klasa obiektu docelowego jest monolitycznie połączona z programem, który wykonuje wywołanie.źródło
ponieważ połączenie wirtualne jest równoważne z
gdzie przy użyciu funkcji innej niż wirtualna kompilator może stale składać pierwszą linię, jest to dereferencja dodatek i wywołanie dynamiczne przekształcone w wywołanie statyczne
pozwala to również wstawić funkcję (ze wszystkimi konsekwencjami optymalizacji)
źródło