Przypadek zachowania 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 funkcji języka. John Moore przedstawia mocne argumenty za utrzymaniem prymitywów w języku Java, porównując proste testy porównawcze Javy, zarówno z prymitywami, jak i bez. Następnie porównuje wydajność Javy z wydajnością Scali, C ++ i JavaScript w konkretnym typie aplikacji, w których 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 porzekadło 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żywaniu typów pierwotnych w Javie, to wydajność, wydajność i 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 wzrost wydajności wynikający z używania typów prymitywnych może się znacznie różnić w zależności od rodzaju aplikacji. Po drugie, w przypadku nieruchomości należy wziąć pod uwagę inne czynniki, mimo że są one zwykle niewielkie w porównaniu z lokalizacją. W przypadku typów prymitywnych jest tylko jeden powód, aby ich używać - wydajność; i tylko wtedy, gdy jest to aplikacja, która może skorzystać na ich użyciu.

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

Włączenie prymitywów w Javie 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 programowym JAX London w listopadzie 2011 r., Że poważnie rozważano usunięcie prymitywów w przyszłej wersji Javy (patrz slajd 41). W tym artykule pokrótce przedstawię prymitywy i system dwóch typów Javy. Korzystając z przykładów kodu i prostych testów porównawczych, przedstawię uzasadnienie, dlaczego prymitywy Java są potrzebne w niektórych typach aplikacji. Porównam również wydajność Javy do wydajności Scali, C ++ i JavaScript.

Pomiar wydajności oprogramowania

Wydajność oprogramowania jest zwykle mierzona w kategoriach czasu i przestrzeni. Czas może być faktycznym czasem pracy, na przykład 3,7 minuty, lub kolejnością wzrostu opartą na wielkości danych wejściowych, na przykład O ( n 2). Podobne miary dotyczą wydajności przestrzeni, która często jest wyrażana w postaci wykorzystania pamięci głównej, ale może również obejmować użycie dysku. Poprawa wydajności zwykle wiąże się z kompromisem czasowo-przestrzennym, ponieważ zmiany mające na celu poprawę czasu często mają szkodliwy wpływ na przestrzeń i odwrotnie. Pomiar kolejności wzrostu zależy od algorytmu, a przełączenie z klas opakowujących na prymitywy nie zmieni wyniku. Ale jeśli chodzi o rzeczywistą wydajność czasu i przestrzeni, użycie prymitywów zamiast klas opakowujących oferuje ulepszenia zarówno w czasie, jak i przestrzeni jednocześnie.

Prymitywy a przedmioty

Jak zapewne już wiesz, czytając ten artykuł, Java ma system podwójnego typu, zwykle określany jako typy prymitywne i typy obiektowe, często w skrócie określane jako prymitywy i obiekty. W Javie istnieje osiem predefiniowanych typów pierwotnych, których nazwy są zastrzeżonymi słowami kluczowymi. Powszechnie stosowane przykłady obejmują int, doublei boolean. Zasadniczo wszystkie inne typy w Javie, w tym wszystkie typy zdefiniowane przez użytkownika, są typami obiektowymi. (Mówię „zasadniczo”, ponieważ typy tablicowe są trochę hybrydowe, ale bardziej przypominają typy obiektów niż typy pierwotne.) Dla każdego typu pierwotnego istnieje odpowiednia klasa opakowania, która jest typem obiektu; przykłady obejmują Integerfor int, Doublefor doublei Booleanfor boolean.

Typy prymitywne są oparte na wartościach, ale typy obiektowe są oparte na odwołaniach i w tym tkwi zarówno siła, jak i źródło kontrowersji związanych z typami pierwotnymi. Aby zilustrować różnicę, rozważ dwie poniższe deklaracje. Pierwsza deklaracja używa typu pierwotnego, a druga klasy opakowania.

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

Używając autoboxingu, funkcji dodanej do JDK 5, mogłem w prosty sposób skrócić drugą deklarację

 Integer n2 = 100; 

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

Różnicę między obiektem prymitywnym n1i opakowującym n2ilustruje diagram na rysunku 1.

John I. Moore, Jr.

Zmienna n1zawiera wartość całkowitą, ale zmienna n2zawiera odniesienie do obiektu i to obiekt przechowuje wartość całkowitą. Ponadto obiekt, do którego się odwołuje, n2zawiera również odniesienie do obiektu klasy Double.

Problem z prymitywami

Zanim spróbuję cię przekonać o potrzebie typów prymitywnych, muszę przyznać, że wiele osób się ze mną nie zgodzi. Sherman Alpert w „Typach prymitywnych uważanych za szkodliwe” argumentuje, ż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 klasy. " Prymitywy i obiekty (w postaci klas opakowujących) zapewniają dwa sposoby obsługi logicznie podobnych typów, ale mają bardzo różną podstawową semantykę. Na przykład, jak należy porównać dwa wystąpienia pod kątem równości? W przypadku typów pierwotnych używa się ==operatora, ale w przypadku obiektów preferowanym wyborem jest wywołanie metodyequals()metoda, która nie jest opcją dla prymitywów. Podobnie, różne semantyki istnieją podczas przypisywania wartości lub przekazywania parametrów. Nawet wartości domyślne są różne; np. 0za intversus nullza Integer.

Więcej informacji na ten temat można znaleźć w poście na blogu Erica Bruno „Nowoczesna prymitywna dyskusja”, która podsumowuje niektóre zalety i wady prymitywów. Wiele dyskusji na temat przepełnienia stosu również skupia się na prymitywach, w tym „Dlaczego ludzie nadal używają typów pierwotnych w Javie?” i „Czy istnieje powód, aby zawsze używać obiektów zamiast prymitywów?” Programmers Stack Exchange organizuje podobną dyskusję zatytułowaną „Kiedy używać prymitywów czy klas w Javie?”.

Wykorzystanie pamięci

A doublew Javie zawsze zajmuje 64 bity w pamięci, ale rozmiar odwołania zależy od wirtualnej maszyny Java (JVM). Na moim komputerze działa 64-bitowa wersja systemu Windows 7 i 64-bitowa maszyna JVM, dlatego odwołanie na moim komputerze zajmuje 64 bity. Oparte na schemacie z Figury 1 byłoby oczekiwać, jeden doubletaki jak n1zajmują 8 bajty (64 bity), a byłoby oczekiwać, jeden Doubletaki jak n2zajmują 24 bajtów - 8 dla odniesienia do obiektu, 8 do doublewartości przechowywanej w obiekt, a 8 jako odwołanie do obiektu klasy Double. Ponadto Java używa dodatkowej pamięci do obsługi czyszczenia pamięci dla typów obiektów, ale nie dla typów pierwotnych. Sprawdźmy to.

Stosując podejście podobne do podejścia Glena McCluskeya w „Typach prymitywnych w Javie a opakowania”, metoda pokazana na liście 1 mierzy liczbę bajtów zajmowanych przez macierz n-na-n (dwuwymiarową tablicę) double.

Listing 1. Obliczanie wykorzystania pamięci typu double

 public static long getBytesUsingPrimitives(int n) { System.gc(); // force garbage collection long memStart = Runtime.getRuntime().freeMemory(); double[][] a = new double[n][n]; // put some random values in the matrix for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) a[i][j] = Math.random(); } long memEnd = Runtime.getRuntime().freeMemory(); return memStart - memEnd; } 

Modyfikując kod na liście 1 z oczywistymi zmianami typu (nie pokazano), możemy również zmierzyć liczbę bajtów zajmowanych przez macierz n-na-n Double. Kiedy testuję te dwie metody na moim komputerze przy użyciu macierzy 1000 na 1000, otrzymuję wyniki przedstawione w tabeli 1 poniżej. Jak pokazano, wersja dla typu pierwotnego doublerówna się nieco ponad 8 bajtów na wpis w macierzy, mniej więcej tego się spodziewałem. Jednak wersja dla typu obiektu Doublewymagała nieco więcej niż 28 bajtów na wpis w macierzy. Zatem w tym przypadku wykorzystanie pamięci Doublejest 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.

Tabela 1. Wykorzystanie pamięci Double versus Double

Wersja Całkowita liczba bajtów Bajty na wpis
Using double 8,380,768 8.381
Using Double 28,166,072 28.166

Runtime performance

To compare the runtime performances for primitives and objects, we need an algorithm dominated by numerical calculations. For this article I have chosen matrix multiplication, and I compute the time required to multiply two 1000-by-1000 matrices. I coded matrix multiplication for double in a straightforward manner as shown in Listing 2 below. While there may be faster ways to implement matrix multiplication (perhaps using concurrency), that point is not really relevant to this article. All I need is common code in two similar methods, one using the primitive double and one using the wrapper class Double. The code for multiplying two matrices of type Double is exactly like that in Listing 2 with the obvious type changes.

Listing 2. Multiplying two matrices of type double

 public static double[][] multiply(double[][] a, double[][] b) { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); int nRows = a.length; int nCols = b[0].length; double[][] result = new double[nRows][nCols]; for (int rowNum = 0; rowNum < nRows; ++rowNum) { for (int colNum = 0; colNum < nCols; ++colNum) { double sum = 0.0; for (int i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; } 

I ran the two methods to multiply two 1000-by-1000 matrices on my computer several times and measured the results. The average times are shown in Table 2. Thus, in this case, the runtime performance of double is more than four times as fast as that of Double. That is simply too much of a difference to ignore.

Table 2. Runtime performance of double versus Double

Version Seconds
Using double 11.31
Using Double 48.48

The SciMark 2.0 benchmark

Do tej pory użyłem pojedynczego, prostego wzorca mnożenia macierzy, aby zademonstrować, że prymitywy mogą dawać znacznie większą wydajność obliczeniową niż obiekty. Aby wzmocnić moje twierdzenia, użyję bardziej naukowego punktu odniesienia. SciMark 2.0 to test porównawczy Java do obliczeń naukowych i numerycznych dostępny w National Institute of Standards and Technology (NIST). Ściągnąłem kod źródłowy tego testu porównawczego i stworzyłem dwie wersje, oryginalną wersję przy użyciu prymitywów, a drugą przy użyciu klas opakowujących. Na drugiej wersji Wymieniłem intz Integeri doublez Double, aby uzyskać pełny efekt za pomocą klas owijki. Obie wersje są dostępne w kodzie źródłowym tego artykułu.

pobierz Benchmarking Java: Pobierz kod źródłowy 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). Thus, larger numbers are better for this benchmark. Table 3 gives the average composite scores from several runs of each version of this benchmark on my computer. As shown, the runtime performances of the two versions of the SciMark 2.0 benchmark were consistent with the matrix multiplication results above in that the version with primitives was almost five times faster than the version using wrapper classes.

Table 3. Runtime performance of the SciMark benchmark

SciMark version Performance (Mflops)
Using primitives 710.80
Using wrapper classes 143.73

You've seen a few variations of Java programs doing numerical calculations, using both a homegrown benchmark and a more scientific one. But how does Java compare to other languages? I'll conclude with a quick look at how Java's performance compares to that of three other programming languages: Scala, C++, and JavaScript.

Benchmarking Scala

Scala to język programowania, który działa w JVM i wydaje się zyskiwać na popularności. Scala ma ujednolicony system typów, co oznacza, że ​​nie rozróżnia prymitywów i obiektów. Według Erika Osheima w klasie typów numerycznych Scali (część 1), Scala używa typów pierwotnych, gdy jest to możliwe, ale w razie potrzeby używa obiektów. Podobnie opis tablic Scali autorstwa Martina Odersky'ego mówi, że „... tablica Scala Array[Int]jest reprezentowana jako Java int[], an Array[Double]jest reprezentowana jako Java double[]...”

Czy to oznacza, że ​​ujednolicony system typów Scali będzie miał wydajność działania porównywalną z typami prymitywnymi Javy? Zobaczmy.