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:
- Skopiuj niektóre rejestry ze stosu, aby umożliwić wywoływanej funkcji korzystanie z tych rejestrów.
- Skopiuj argumenty do predefiniowanych lokalizacji, aby wywoływana funkcja mogła je znaleźć bez względu na to, gdzie jest wywołana.
- Naciśnij adres zwrotny.
- Rozgałęzienie / skok do kodu funkcji, który jest adresem czasu kompilacji, a zatem zapisanym na stałe w pliku binarnym przez kompilator / linker.
- Uzyskaj wartość zwrotną ze wstępnie zdefiniowanej lokalizacji i przywróć rejestry, których chcemy użyć.
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 ...
- Uzyskaj od obiektu wskaźnik vtable, który wskazuje na tablicę wskaźników funkcji (adresów funkcji), po jednym dla każdej funkcji wirtualnej, z obiektu.
- Uzyskaj odpowiedni adres funkcji z tabeli vt do rejestru (indeks, w którym przechowywany jest poprawny adres funkcji, jest ustalany w czasie kompilacji).
- Przejdź do adresu w tym rejestrze zamiast skakać na adres zakodowany na stałe.
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.