Slučaj za držanje primitiva u Javi

Primitivi su dio programskog jezika Java od njegovog početnog izdanja 1996. godine, ali ipak ostaju jedna od kontroverznijih jezičnih značajki. John Moore daje snažne argumente za zadržavanje primitiva u jeziku Java uspoređujući jednostavna mjerila Java, i sa primitivima i bez njih. Zatim uspoređuje izvedbu Jave s izvedbom Scale, C ++ i JavaScript u određenoj vrsti aplikacija, gdje primitivi čine značajnu razliku.

Pitanje : Koja su tri najvažnija čimbenika u kupnji nekretnina?

Odgovor : Mjesto, mjesto, mjesto.

Ova stara i često korištena izreka želi implicirati da mjesto u potpunosti dominira svim ostalim čimbenicima kada je riječ o nekretninama. U sličnom argumentu, tri najvažnija čimbenika koja treba uzeti u obzir za upotrebu primitivnih tipova u Javi su izvedba, izvedba, izvedba. Dvije su razlike između argumenta za nekretnine i argumenta za primitivce. Prvo, kod nekretnina, lokacija dominira u gotovo svim situacijama, ali dobici u izvedbi korištenjem primitivnih tipova mogu se uvelike razlikovati od jedne do druge vrste aplikacija. Drugo, kod nekretnina treba razmotriti i druge čimbenike, iako su oni obično manje u usporedbi s lokacijom. Kod primitivnih tipova postoji samo jedan razlog za njihovu upotrebu - izvedba; i to samo ako je aplikacija vrsta koja može imati koristi od njihove upotrebe.

Primitivi nude malu vrijednost za većinu poslovnih i internetskih aplikacija koje koriste model programiranja klijent-poslužitelj s bazom podataka na pozadini. Ali performanse aplikacija kojima dominiraju numerički izračuni mogu uvelike imati koristi od primitiva.

Uključivanje primitiva u Javu bila je jedna od kontroverznijih odluka o dizajnu jezika, o čemu svjedoči broj članaka i postova na forumu koji se odnose na ovu odluku. Simon Ritter je u svom glavnom obraćanju u Londonu u studenom 2011. primijetio da se ozbiljno razmatra uklanjanje primitiva u budućoj verziji Jave (vidi slajd 41). U ovom ću članku ukratko predstaviti primitive i Javin dvostruki tip sustava. Koristeći uzorke koda i jednostavne referentne vrijednosti, obrazložit ću zašto su Java primitivi potrebni za određene vrste aplikacija. Također ću usporediti izvedbu Jave s izvedbom Scale, C ++ i JavaScript.

Mjerenje performansi softvera

Učinak softvera obično se mjeri vremenski i prostorno. Vrijeme može biti stvarno vrijeme izvođenja, poput 3,7 minuta, ili redoslijed rasta na temelju veličine unosa, poput O ( n 2). Slične mjere postoje i za performanse prostora, koje se često izražavaju u smislu korištenja glavne memorije, ali se mogu proširiti i na upotrebu diska. Poboljšanje izvedbe obično uključuje vremensko-prostorni kompromis u kojem promjene radi poboljšanja vremena često imaju štetan učinak na prostor i obrnuto. Mjerenje redoslijeda rasta ovisi o algoritmu, a prelazak s klasa omota na primitivne neće promijeniti rezultat. Ali što se tiče stvarnih vremenskih i prostornih performansi, upotreba primitiva umjesto klasa omota nudi istodobna poboljšanja i u vremenu i u prostoru.

Primitivci naspram predmeta

Kao što vjerojatno već znate čitate li ovaj članak, Java ima sustav dvostrukog tipa, koji se obično naziva primitivnim tipovima i vrstama objekata, često skraćeno jednostavno kao primitivi i objekti. U Javi je unaprijed definirano osam primitivnih tipova, a njihova su imena rezervirane ključne riječi. Obično se koristi primjeri uključuju int, doublei boolean. U osnovi svi ostali tipovi u Javi, uključujući sve korisnički definirane tipove, su tipovi objekata. (Kažem "u biti", jer su tipovi polja pomalo hibridni, ali mnogo su sličniji vrstama objekata nego primitivnim vrstama.) Za svaki primitivni tip postoji odgovarajuća klasa omota koja je objektni tip; primjeri uključuju Integerfor int, Doublefor doublei Booleanfor boolean.

Primitivni tipovi temelje se na vrijednosti, ali tipovi objekata temelje se na referenci, i u tome leži i snaga i izvor kontroverze primitivnih tipova. Da biste ilustrirali razliku, razmotrite dvije izjave u nastavku. Prva deklaracija koristi primitivni tip, a druga koristi klasu omota.

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

Koristeći autoboxing, značajku dodanu JDK 5, mogao bih skratiti drugu deklaraciju na jednostavno

 Integer n2 = 100; 

ali temeljna semantika se ne mijenja. Autoboxing pojednostavljuje upotrebu klasa omotača i smanjuje količinu koda koji programer mora napisati, ali ne mijenja ništa tijekom izvođenja.

Razlika između primitiva n1i omotača n2prikazana je dijagramom na slici 1.

John I. Moore, ml.

Varijabla n1sadrži cjelobrojnu vrijednost, ali varijabla n2sadrži referencu na objekt, a objekt je taj koji ima cjelobrojnu vrijednost. Uz to, objekt na koji se poziva n2također sadrži referencu na objekt klase Double.

Problem s primitivcima

Prije nego što vas pokušam uvjeriti u potrebu za primitivnim tipovima, trebao bih priznati da se mnogi ljudi neće složiti sa mnom. Sherman Alpert u knjizi "Primitivni tipovi koji se smatraju štetnima" tvrdi da su primitivi štetni jer miješaju "proceduralnu semantiku u inače jedinstveni objektno orijentirani model. Primitivi nisu prvoklasni objekti, ali postoje u jeziku koji uključuje, prije svega, prvo - predmeti klase. " Primitivi i objekti (u obliku klasa omotača) pružaju dva načina rukovanja logički sličnim vrstama, ali imaju vrlo različitu temeljnu semantiku. Na primjer, kako treba usporediti dva slučaja radi jednakosti? Za primitivne tipove koristi se ==operator, ali za objekte je preferirani izbor pozivanjeequals()metoda, koja nije opcija za primitivce. Slično tome, različita semantika postoji pri dodjeli vrijednosti ili prosljeđivanju parametara. Čak su i zadane vrijednosti različite; npr. 0za intnasuprot nullfor Integer.

Za više informacija o ovom pitanju, pogledajte post na blogu Erica Brune, "Moderna primitivna rasprava", koji sažima neke prednosti i nedostatke primitiva. Niz rasprava o Stack Overflowu također se fokusira na primitive, uključujući "Zašto ljudi još uvijek koriste primitivne tipove u Javi?" i "Postoji li razlog da se uvijek koriste Objekti umjesto primitiva?". Programmers Stack Exchange domaćin je slične rasprave pod nazivom "Kada koristiti primitive vs class u Javi?".

Korištenje memorije

A doubleu Javi uvijek zauzima 64 bita u memoriji, ali veličina reference ovisi o Java virtualnom stroju (JVM). Moje računalo pokreće 64-bitnu verziju sustava Windows 7 i 64-bitni JVM, pa referenca na mom računalu zauzima 64 bita. Na temelju dijagrama na slici 1. ja bi se očekivati jedan doublekao n1da zauzme 8 bajta (64 bita), a ja bi se očekivati jedan Doublekao n2da zauzme 24 bajta - 8 za poziv na objekt, 8 za doublevrijednosti pohranjene u objekt i 8 za referencu na objekt klase za Double. Osim toga, Java koristi dodatnu memoriju za podršku prikupljanju smeća za tipove objekata, ali ne i za primitivne tipove. Provjerimo.

Koristeći pristup sličan pristupu Glena McCluskeyja u "Java primitivnim tipovima u odnosu na omote", metoda prikazana na popisu 1 mjeri broj bajtova zauzetih n-by-n matricom (dvodimenzionalni niz) od double.

Popis 1. Izračunavanje iskorištenosti memorije tipa 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; } 

Modificirajući kôd na popisu 1 očitim promjenama tipa (nije prikazano), također možemo izmjeriti broj bajtova zauzetih matricom n-by-n od Double. Kada testiram ove dvije metode na svom računalu pomoću matrica 1000 na 1000, dobivam rezultate prikazane u donjoj tablici 1. Kao što je ilustrirano, verzija za primitivni tip doubleiznosi nešto više od 8 bajtova po unosu u matricu, otprilike ono što sam očekivao. Međutim, verzija za tip objekta Doublezahtijevala je malo više od 28 bajtova po unosu u matricu. Stoga je u ovom slučaju iskorištenost memorije Doubleviše od tri puta veća od upotrebe memorije double, što ne bi trebalo biti iznenađenje za svakoga tko razumije raspored memorije prikazan na gornjoj slici 1.

Tablica 1. Korištenje memorije dvostruko u odnosu na dvostruko

Verzija Ukupno bajtova Bajtova po unosu
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 sada sam koristio jednostavan, jednostavan mjerilac umnožavanja matrica da bih pokazao da primitivi mogu postići znatno veće računalne performanse od objekata. Da bih ojačao svoje tvrdnje, poslužit ću se više znanstvenim mjerilom. SciMark 2.0 Java je referentna vrijednost za znanstveno i numeričko računanje dostupna od Nacionalnog instituta za standarde i tehnologiju (NIST). Preuzeo sam izvorni kod za ovu referentnu vrijednost i stvorio dvije verzije, izvornu verziju koja koristi primitive i drugu verziju pomoću klasa omota. Za drugu verziju sam zamijenio intsa Integeri doublesa Doublekako bih postigao puni efekt korištenja klasa omota. Obje su verzije dostupne u izvornom kodu za ovaj članak.

preuzmi Benchmarking Java: preuzmi izvorni kod 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 je programski jezik koji radi na JVM-u i čini se da dobiva na popularnosti. Scala ima jedinstveni sustav tipa, što znači da ne pravi razliku između primitiva i objekata. Prema Eriku Osheimu iz Scala-ove klase numeričkih tipova (Pt. 1), Scala koristi primitivne tipove kad je to moguće, ali po potrebi će koristiti objekte. Slično tome, opis Scala-ovih nizova Martina Oderskog kaže da "... niz Scala Array[Int]predstavljen je kao Java int[], a Array[Double]predstavljen kao Java double[]..."

Pa znači li to da će Scalain objedinjeni sustav tipova imati izvedbene performanse usporedive s Java primitivnim tipovima? Da vidimo.