Przykład na zachowanie prymitywów w Javie

prymitywy są częścią języka programowania Java od jego pierwszego wydania w 1996 roku, a mimo to pozostają jedną z bardziej kontrowersyjnych cech języka. John Moore przedstawia mocne argumenty za utrzymaniem prymitywów w języku Java, porównując proste benchmarki Javy, zarówno z prymitywami, jak i bez nich. Następnie porównuje wydajność Javy do wydajności Scali, C++ i JavaScript w konkretnym typie aplikacji, gdzie prymitywy robią znaczącą różnicę.

pytanie: Jakie są trzy najważniejsze czynniki przy zakupie nieruchomości?
odpowiedź: Lokalizacja, Lokalizacja, Lokalizacja.

to stare i często używane powiedzenie ma sugerować, że lokalizacja całkowicie dominuje nad wszystkimi innymi czynnikami, jeśli chodzi o nieruchomości. W podobnym argumencie trzy najważniejsze czynniki, które należy wziąć pod uwagę przy użyciu prymitywnych typów w Javie to wydajność, wydajność, wydajność. Istnieją dwie różnice między argumentem za nieruchomościami a argumentem za prymitywami. Po pierwsze, w przypadku nieruchomości Lokalizacja dominuje w prawie wszystkich sytuacjach, ale zyski z używania prymitywnych typów mogą się znacznie różnić w zależności od rodzaju aplikacji. Po drugie, w przypadku nieruchomości istnieją inne czynniki, które należy wziąć pod uwagę, mimo że są one zwykle niewielkie w porównaniu z lokalizacją. W przypadku typów prymitywnych istnieje tylko jeden powód, aby z nich korzystać — wydajność; i tylko wtedy, gdy aplikacja jest rodzajem, który może skorzystać z ich użycia.

Primitives oferują niewielką wartość dla większości aplikacji biznesowych i internetowych, które używają modelu programowania klient-serwer z bazą danych na zapleczu. Ale wydajność aplikacji, które są zdominowane przez obliczenia numeryczne, może znacznie skorzystać z zastosowania prymitywów.

włączenie prymitywów do Javy było jedną z bardziej kontrowersyjnych decyzji dotyczących projektowania języka, o czym świadczy liczba artykułów i postów na forum związanych z tą decyzją. Simon Ritter zauważył w swoim przemówieniu Jax London w listopadzie 2011, że poważnie rozważano usunięcie prymitywów w przyszłej wersji Javy (patrz slajd 41). W tym artykule krótko przedstawię prymitywy i podwójny system Javy. Korzystając z próbek kodu i prostych benchmarków, wyjaśnię, dlaczego prymitywy Java są potrzebne dla niektórych typów aplikacji. Porównam również wydajność Javy do wydajności Scali, C++ i JavaScript.

prymitywy kontra obiekty

jak zapewne już wiesz, jeśli czytasz ten artykuł, Java ma system dwóch typów, zwykle nazywany typami prymitywnymi i typami obiektów, często skracany po prostu jako prymitywy i Obiekty. Istnieje osiem prymitywnych typów predefiniowanych w Javie, a ich nazwy są zarezerwowanymi słowami kluczowymi. Często używane przykłady to int, doublei boolean. Zasadniczo wszystkie inne typy w Javie, w tym wszystkie typy zdefiniowane przez użytkownika, są typami obiektów. (Mówię „zasadniczo”, ponieważ typy tablic są trochę hybrydowe, ale są bardziej podobne do typów obiektów niż typów prymitywnych.) Dla każdego typu prymitywnego istnieje odpowiednia Klasa wrapper, która jest typem obiektu; przykłady obejmują Integer dla int, Double dla double i Boolean dla boolean.

typy prymitywne są oparte na wartościach, ale typy obiektowe są oparte na referencjach i w tym tkwi zarówno moc, jak i źródło kontrowersji typów prymitywnych. Aby zilustrować różnicę, rozważ dwie poniższe deklaracje. Pierwsza deklaracja używa prymitywnego typu, a druga używa klasy wrapper.

int n1 = 100;Integer n2 = new Integer(100);

korzystając z autoboxingu, funkcji dodanej do JDK 5, mogłem skrócić drugą deklarację do po prostu

Integer n2 = 100;

, ale podstawowa semantyka się nie zmienia. Autoboxing upraszcza korzystanie z klas opakowywania i zmniejsza ilość kodu, który programista musi napisać, ale nic nie zmienia w czasie wykonywania.

różnica między prymitywnym n1 a obiektem owijania n2 jest zilustrowana diagramem na rysunku 1.

John I. Moore, Jr.

Rysunek 1. Układ pamięci prymitywów a obiektów

zmienna n1 posiada wartość całkowitą, ale zmienna n2 zawiera odniesienie do obiektu i jest to obiekt, który posiada wartość całkowitą. Ponadto obiekt, do którego odwołuje się n2, zawiera również odniesienie do obiektu klasy Double.

problem z prymitywami

zanim spróbuję przekonać was o potrzebie prymitywów, muszę przyznać, że wiele osób nie zgodzi się ze mną. Sherman Alpert w” Primitive types considered szkodliwe „twierdzi, że prymitywy są szkodliwe, ponieważ mieszają” semantykę proceduralną w jednolity model obiektowy. Prymitywy nie są obiektami pierwszej klasy, ale istnieją w języku, który obejmuje przede wszystkim obiekty pierwszej klasy.”Prymitywy i obiekty (w postaci klas wrapper) zapewniają dwa sposoby obsługi logicznie podobnych typów, ale mają bardzo odmienną semantykę podstawową. Na przykład, jak dwie instancje powinny być porównywane pod kątem równości? Dla typów prymitywnych używa się operatora ==, ale dla obiektów preferowanym wyborem jest wywołanie metody equals(), która nie jest opcją dla typów podstawowych. Podobnie istnieją różne semantyki przy przypisywaniu wartości lub przekazywaniu parametrów. Nawet wartości domyślne są różne; na przykład, 0 dla int versus null dla Integer.

aby uzyskać więcej informacji na ten temat, zobacz wpis na blogu Erica Bruno, „a modern primitive discussion”, który podsumowuje niektóre zalety i wady prymitywów. Wiele dyskusji na temat przepełnienia stosu koncentruje się również na prymitywach, w tym ” dlaczego ludzie nadal używają prymitywnych typów w Javie?”i” czy jest powód, aby zawsze używać przedmiotów zamiast prymitywów?.”Programiści Stack Exchange prowadzą podobną dyskusję pt.” kiedy używać klasy primitive vs w Javie?”.

wykorzystanie pamięci

a double w Javie zawsze zajmuje 64 bity w pamięci, ale rozmiar referencji zależy od maszyny wirtualnej Javy (JVM). Mój komputer uruchamia 64-bitową wersję systemu Windows 7 i 64-bitowy JVM, a zatem odniesienie na moim komputerze zajmuje 64 bity. Opierając się na schemacie na rysunku 1, spodziewałbym się, że pojedynczy double, taki jak n1, zajmie 8 bajtów (64 bity), a pojedynczy Double, taki jak n2, zajmie 24 bajty — 8 dla odniesienia do obiektu, 8 dla wartości double przechowywanej w obiekcie i 8 dla odniesienia do obiektu klasy dla Double. Ponadto Java używa dodatkowej pamięci do obsługi usuwania śmieci typów obiektów, ale nie typów prymitywnych. Sprawdźmy to.

zastosowanie podejścia podobnego do podejścia Glena Mccluskeya w „Java primitive types vs. wrappers, ” metoda pokazana na liście 1 mierzy liczbę bajtów zajmowanych przez macierz N na N (dwuwymiarową tablicę) double.

notowanie 1. Obliczanie wykorzystania pamięci typu double

modyfikując kod w liście 1 z oczywistymi zmianami typu (nie pokazanymi), możemy również zmierzyć liczbę bajtów zajmowanych przez macierz n-by-n Double. Kiedy testuję te dwie metody na moim komputerze przy użyciu matryc 1000 na 1000, otrzymuję wyniki pokazane w tabeli 1 poniżej. Jak pokazano, wersja dla typu prymitywnego double równa się nieco więcej niż 8 bajtów na wpis w macierzy, mniej więcej tego, czego się spodziewałem. Jednak wersja dla obiektów typu Double wymagała nieco więcej niż 28 bajtów na wpis w macierzy. Tak więc w tym przypadku wykorzystanie pamięci Double jest ponad trzykrotnie większe niż wykorzystanie pamięci double, co nie powinno być zaskoczeniem dla każdego, kto rozumie układ pamięci przedstawiony na rysunku 1 powyżej.

wydajność Runtime

aby porównać wydajność runtime dla prymitywów i obiektów, potrzebujemy algorytmu zdominowanego przez obliczenia numeryczne. Do tego artykułu wybrałem mnożenie macierzy i obliczam czas potrzebny do mnożenia dwóch macierzy 1000 na 1000. Kodowałem mnożenie macierzy dla double w prosty sposób, jak pokazano w liście 2 poniżej. Chociaż mogą istnieć szybsze sposoby implementacji mnożenia macierzy (być może przy użyciu współbieżności), ten punkt nie jest tak naprawdę istotny w tym artykule. Wszystko czego potrzebuję to wspólny kod w dwóch podobnych metodach, jedna używając prymitywnej double i jedna używając klasy wrapper Double. Kod mnożenia dwóch macierzy typu Double jest dokładnie taki jak w liście 2 z oczywistymi zmianami typów.

notowanie 2. Mnożenie dwóch macierzy typu double

przeprowadziłem dwie metody mnożenia dwóch macierzy 1000 na 1000 na moim komputerze kilka razy i zmierzyłem wyniki. Średnie czasy przedstawiono w tabeli 2. Tak więc w tym przypadku wydajność runtime double jest ponad cztery razy szybsza niż wydajność Double. To po prostu zbyt duża różnica, by ją zignorować.

Benchmark SciMark 2.0

do tej pory wykorzystałem pojedynczy, prosty benchmark mnożenia macierzy, aby wykazać, że prymitywy mogą uzyskać znacznie większą wydajność obliczeniową niż obiekty. Aby wzmocnić moje twierdzenia, użyję bardziej naukowego wzorca. SciMark 2.0 to benchmark Java do obliczeń naukowych i numerycznych dostępny w National Institute of Standards and Technology (NIST). Pobrałem kod źródłowy dla tego benchmarka i stworzyłem dwie wersje, oryginalną używając primitives i drugą używając klas wrapper. Dla drugiej wersji zamieniłem int na Integer i double na Double, aby uzyskać pełny efekt używania klas wrapper. Obie wersje są dostępne w kodzie źródłowym tego artykułu.

Pobierz

John I. Moore, Jr.

the SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). W związku z tym większe liczby są lepsze dla tego wskaźnika. Tabela 3 podaje średnie wyniki złożone z kilku uruchomień każdej wersji tego testu na moim komputerze. Jak pokazano, wydajność obu wersji benchmarku SciMark 2.0 była zgodna z powyższymi wynikami mnożenia macierzy, ponieważ wersja z prymitywami była prawie pięć razy szybsza niż wersja z klasami owijania.

widziałeś kilka odmian programów Java wykonujących obliczenia numeryczne, używając zarówno domowego benchmarka, jak i bardziej naukowego. Ale jak Java porównuje się z innymi językami? Zakończę krótkim spojrzeniem na to, jak wydajność Javy porównuje się do wydajności trzech innych języków programowania: Scala, C++ i JavaScript.

Benchmarking Scala

Scala jest językiem programowania, który działa na JVM i wydaje się zyskiwać na popularności. Scala ma zunifikowany system typów, co oznacza, że nie rozróżnia prymitywów i obiektów. Według Erika Osheima w klasie typów numerycznych Scali (pt. 1), Scala używa typów prymitywnych, jeśli to możliwe, ale w razie potrzeby użyje obiektów. Podobnie, Martin Odersky opis tablic Scali mówi, że”… tablica Scala Array jest reprezentowana jako Java int, Array jest reprezentowana jako Java double …”

czy to oznacza, że unified type system Scali będzie miał wydajność runtime porównywalną z prymitywnymi typami Javy? Zobaczmy.

nie jestem tak biegły w Scali jak w Javie, ale próbowałem przekonwertować kod testu mnożenia macierzy bezpośrednio z Javy na Scal. Wynik pokazano na liście 3 poniżej. Kiedy uruchomiłem wersję benchmarku Scala na moim komputerze, średnia wynosiła 12,30 sekundy, co stawia wydajność Scali bardzo blisko wydajności Javy z prymitywami. Ten wynik jest znacznie lepszy niż się spodziewałem i wspiera twierdzenia o tym, jak Scala radzi sobie z typami numerycznymi.

Pobierz

John I. Moore, Jr.

Lista 3. Mnożenie dwóch macierzy w Scali

Benchmarking C++

ponieważ C++ działa bezpośrednio na „gołym metalu”, a nie na maszynie wirtualnej, można naturalnie oczekiwać, że C++ będzie działał szybciej niż Java. Co więcej, wydajność Javy jest nieznacznie zmniejszona przez fakt, że Java sprawdza dostęp do tablic, aby upewnić się, że każdy indeks znajduje się w granicach zadeklarowanych dla tablicy, podczas gdy C++ nie (Funkcja C++, która może prowadzić do przepełnienia buforów, które mogą być wykorzystywane przez hakerów). Uważam, że C++ jest nieco bardziej niezręczny niż Java w radzeniu sobie z podstawowymi tablicami dwuwymiarowymi, ale na szczęście wiele z tej niezręczności można ukryć wewnątrz prywatnych części klasy. Dla C++ stworzyłem prostą wersję klasy Matrix i przeciążyłem operator * do mnożenia dwóch macierzy, ale podstawowy algorytm mnożenia macierzy został skonwertowany bezpośrednio z wersji Javy. Kod źródłowy C++ jest pokazany na liście 4.

Pobierz

John I. Moore, Jr.

Lista 4. Mnożąc dwie macierze w C++

używając Eclipse CDT (Eclipse for C++ Developers) z kompilatorem MinGW c++, możliwe jest tworzenie zarówno wersji debugowania, jak i wydawania aplikacji. Aby przetestować C++ uruchomiłem wersję release kilka razy i uśredniłem wyniki. Zgodnie z oczekiwaniami, C++ działał zauważalnie szybciej na tym prostym benchmarku, średnio 7.58 sekund na moim komputerze. Jeśli wydajność surowa jest głównym czynnikiem wyboru języka programowania, to C++ jest językiem z wyboru dla aplikacji intensywnie wykorzystujących liczby.

Benchmarking JavaScript

ok, ten mnie zaskoczył. Biorąc pod uwagę, że JavaScript jest bardzo dynamicznym językiem, spodziewałem się, że jego wydajność będzie najgorsza ze wszystkich, nawet gorsza niż Java z klasami wrapper. Ale w rzeczywistości wydajność JavaScript była znacznie bliższa wydajności Javy z prymitywami. Aby przetestować JavaScript zainstalowałem Node.js, silnik JavaScript o reputacji bardzo wydajny. Wyniki osiągnęły średnio 15,91 sekundy. Lista 5 pokazuje wersję JavaScript testu mnożenia macierzy, który uruchomiłem na węźle.js

Pobierz

John I. Moore, Jr.

Lista 5. Mnożenie dwóch macierzy w JavaScript

podsumowując

kiedy Java pojawiła się po raz pierwszy jakieś 18 lat temu, nie był to najlepszy język z punktu widzenia wydajności dla aplikacji, które są zdominowane przez obliczenia numeryczne. Jednak z biegiem czasu, wraz z postępem technologicznym w obszarach takich jak kompilacja just-in-time (JIT) (aka kompilacja adaptacyjna lub dynamiczna), wydajność Javy dla tego rodzaju aplikacji jest teraz porównywalna z wydajnością języków, które są kompilowane do kodu natywnego, gdy używane są typy prymitywne.

co więcej, prymitywy eliminują potrzebę usuwania śmieci, zapewniając tym samym kolejną przewagę wydajności prymitywów nad typami obiektów. Tabela 4 podsumowuje wydajność wykonania testu mnożenia macierzy na moim komputerze. Inne czynniki, takie jak łatwość konserwacji, przenośność i doświadczenie programistów sprawiają, że Java jest lepszym wyborem dla wielu takich aplikacji.

jak wcześniej wspomniano, Oracle wydaje się poważnie rozważać usunięcie prymitywów w przyszłej wersji Javy. O ile kompilator Javy nie jest w stanie wygenerować kodu o wydajności porównywalnej z wydajnością prymitywów, myślę, że ich usunięcie z Javy wykluczyłoby użycie Javy dla pewnych klas aplikacji, a mianowicie tych aplikacji zdominowanych przez obliczenia numeryczne. W tym artykule wykorzystałem prosty benchmark oparty na mnożeniu macierzy i bardziej naukowy benchmark, SciMark 2.0, aby argumentować ten punkt.

o autorze

John I. Moore, Jr., Profesor matematyki i Informatyki na Cytadeli, ma szerokie doświadczenie zarówno w przemyśle, jak i w środowisku akademickim, ze szczególną wiedzą w obszarach technologii obiektowej, inżynierii oprogramowania i Matematyki Stosowanej. Przez ponad trzy dekady projektował i rozwijał oprogramowanie wykorzystujące relacyjne bazy danych i kilka języków wysokiego rzędu, a od wersji 1.1 intensywnie pracował w Javie. Ponadto opracował i prowadził liczne kursy akademickie i seminaria przemysłowe na zaawansowane tematy z informatyki.

Czytaj dalej

  1. Paul Krill napisał o dalekosiężnych planach Oracle dla Javy w „Oracle lays out long-range Java intentions” (JavaWorld, marzec 2012). Ten artykuł, wraz z powiązanym wątkiem komentarzy, zmotywował mnie do napisania tej obrony prymitywów.
  2. Szymon Guz pisze o swoich wynikach w benchmarkingu typów prymitywnych i klas wrapperów w „Primitives and objects benchmark in Java” (SimonOnSoftware, styczeń 2011).
  3. na stronie wsparcia dla programowania — zasady i praktyka przy użyciu C++ (Addison-Wesley, 2009), twórca C++ Bjarne Stroustrup dostarcza implementację dla klasy macierzy, która jest znacznie bardziej kompletna niż ta towarzysząca temu artykułowi.
  4. John Rose, Brian Goetz i Guy Steele omawiają koncepcję zwaną typami wartości w ” stanie wartości „(OpenJDK.net, Kwiecień 2014). Typy wartości można traktować jako niezmienne typy agregatów definiowane przez Użytkownika bez tożsamości, poprzez łączenie właściwości zarówno obiektów, jak i obiektów pierwotnych. Mantra dla typów wartości to ” kody jak klasa, działa jak int.”

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.