Czy jest jakiś narzut, gdy rzucamy obiekty jednego typu na inny? A może kompilator po prostu rozwiązuje wszystko i nie ma żadnych kosztów w czasie wykonywania?
Czy to sprawa ogólna, czy są różne przypadki?
Na przykład załóżmy, że mamy tablicę Object [], gdzie każdy element może mieć inny typ. Ale zawsze wiemy na pewno, że, powiedzmy, element 0 to Double, a element 1 to String. (Wiem, że to zły projekt, ale załóżmy, że musiałem to zrobić.)
Czy informacje o typach Javy są nadal dostępne w czasie wykonywania? Albo wszystko jest zapomniane po kompilacji, a jeśli zrobimy (Double) elementy [0], po prostu podążymy za wskaźnikiem i zinterpretujemy te 8 bajtów jako podwójne, cokolwiek to jest?
Nie mam pewności, jak są wykonywane typy w Javie. Jeśli masz jakieś rekomendacje dotyczące książek lub artykułu, to też dziękuję.
Odpowiedzi:
Istnieją 2 rodzaje odlewania:
Rzutowanie niejawne , gdy rzutujesz z typu na szerszy typ, co jest wykonywane automatycznie i nie ma narzutów:
Wyraźne rzucanie, gdy przechodzisz od szerszego typu do węższego. W tym przypadku musisz jawnie użyć rzutowania w ten sposób:
W tym drugim przypadku występuje obciążenie w czasie wykonywania, ponieważ oba typy muszą zostać sprawdzone, a jeśli rzutowanie nie jest możliwe, JVM musi zgłosić wyjątek ClassCastException.
Zaczerpnięte z JavaWorld: Koszt odlewania
źródło
((String)o).someMethodOfCastedClass()
Aby uzyskać rozsądną implementację języka Java:
Każdy obiekt ma nagłówek zawierający między innymi wskaźnik do typu środowiska wykonawczego (na przykład
Double
lubString
, ale nigdy nie może to byćCharSequence
lubAbstractList
). Zakładając, że kompilator środowiska uruchomieniowego (ogólnie HotSpot w przypadku Sun'a) nie może statycznie określić typu, a wygenerowany kod maszynowy musi przeprowadzić pewne sprawdzenia.Najpierw należy odczytać wskaźnik do typu środowiska wykonawczego. Jest to i tak konieczne do wywołania metody wirtualnej w podobnej sytuacji.
W przypadku rzutowania na typ klasy wiadomo dokładnie, ile jest superklas, dopóki nie trafisz
java.lang.Object
, więc typ można odczytać ze stałym przesunięciem od wskaźnika typu (właściwie pierwsza osiem w HotSpot). Ponownie jest to analogiczne do odczytu wskaźnika metody dla metody wirtualnej.Wtedy odczytana wartość wymaga porównania z oczekiwanym statycznym typem rzutowania. W zależności od architektury zestawu instrukcji, inna instrukcja będzie musiała rozgałęzić się (lub spowodować błąd) w nieprawidłowej gałęzi. ISA, takie jak 32-bitowe ARM, mają instrukcje warunkowe i mogą mieć możliwość przejścia smutnej ścieżki przez szczęśliwą ścieżkę.
Interfejsy są trudniejsze z powodu wielokrotnego dziedziczenia interfejsu. Zwykle ostatnie dwa rzuty do interfejsów są buforowane w typie środowiska uruchomieniowego. W bardzo wczesnych dniach (ponad dziesięć lat temu) interfejsy były nieco powolne, ale to już nie ma znaczenia.
Miejmy nadzieję, że widzisz, że tego rodzaju rzeczy są w dużej mierze nieistotne dla wydajności. Twój kod źródłowy jest ważniejszy. Jeśli chodzi o wydajność, największym hitem w twoim scenariuszu mogą być chybienia w pamięci podręcznej spowodowane ściganiem wskaźników obiektów w całym miejscu (informacje o typie będą oczywiście powszechne).
źródło
Kompilator nie zwraca uwagi na typy poszczególnych elementów tablicy. Po prostu sprawdza, czy typ każdego wyrażenia elementu można przypisać do typu elementu tablicy.
Niektóre informacje są przechowywane w czasie wykonywania, ale nie statyczne typy poszczególnych elementów. Możesz to stwierdzić patrząc na format pliku klasy.
Teoretycznie jest możliwe, że kompilator JIT mógłby użyć „analizy ucieczki”, aby wyeliminować niepotrzebne sprawdzanie typu w niektórych przypisaniach. Jednak zrobienie tego w stopniu, który sugerujesz, wykraczałoby poza granice realistycznej optymalizacji. Korzyści z analizy typów poszczególnych elementów byłyby zbyt małe.
Poza tym ludzie i tak nie powinni pisać takiego kodu aplikacji.
źródło
(float) Math.toDegrees(theta)
Czy również tutaj będzie znaczący narzut?Wywoływana jest instrukcja kodu bajtowego do wykonywania rzutowania w czasie wykonywania
checkcast
. Możesz zdemontować kod Java za pomocą,javap
aby zobaczyć, jakie instrukcje są generowane.W przypadku tablic Java przechowuje informacje o typie w czasie wykonywania. W większości przypadków kompilator wyłapie za Ciebie błędy typu, ale zdarzają się przypadki, w których napotkasz a
ArrayStoreException
podczas próby zapisania obiektu w tablicy, ale typ nie pasuje (a kompilator go nie przechwycił) . Specyfikacja języka Java podaje następujący przykład:Point[] pa = cpa
jest ważny, ponieważColoredPoint
jest podklasą Point, alepa[0] = new Point()
nie jest prawidłowy.Jest to przeciwieństwo typów ogólnych, w przypadku których nie ma informacji o typie przechowywanych w czasie wykonywania. W
checkcast
razie potrzeby kompilator wstawia instrukcje.Ta różnica w wpisywaniu typów ogólnych i tablic powoduje, że często nie nadaje się do mieszania tablic i typów ogólnych.
źródło
W teorii wprowadzono narzut. Jednak nowoczesne maszyny JVM są inteligentne. Każda implementacja jest inna, ale nie jest nierozsądne założenie, że może istnieć implementacja zoptymalizowana przez JIT w celu wykluczenia rzutowania sprawdzeń, kiedy mogłaby zagwarantować, że nigdy nie będzie konfliktu. Jeśli chodzi o konkretne maszyny JVM, które to oferują, nie mogę ci powiedzieć. Muszę przyznać, że sam chciałbym poznać specyfikę optymalizacji JIT, ale powinni się nimi martwić inżynierowie JVM.
Morał tej historii polega na napisaniu najpierw zrozumiałego kodu. Jeśli doświadczasz spowolnień, przedstaw profil i zidentyfikuj problem. Szanse są dobre, że nie będzie to spowodowane rzucaniem. Nigdy nie poświęcaj czystego, bezpiecznego kodu, próbując go zoptymalizować, AŻ NIE WIESZ, ŻE POTRZEBUJESZ.
źródło