Primitives har vært en del av Java-programmeringsspråket siden den første utgivelsen i 1996, og likevel er de fortsatt en av de mer kontroversielle språkfunksjonene. John Moore gir en sterk argumentasjon for å beholde primitiver på Java-språket ved å sammenligne enkle Java-referanser, både med og uten primitiver. Deretter sammenligner han ytelsen til Java med Scala, C ++ og JavaScript i en bestemt type applikasjon, der primitiver utgjør en betydelig forskjell.
Spørsmål: Hva er de tre viktigste faktorene ved kjøp av eiendom?
Svar: Beliggenhet, beliggenhet, beliggenhet.
Dette gamle og ofte brukte ordtaket er ment å antyde at beliggenhet dominerer alle andre faktorer når det gjelder eiendom. I et lignende argument er de tre viktigste faktorene du må vurdere for å bruke primitive typer i Java ytelse, ytelse, ytelse. Det er to forskjeller mellom argumentet for eiendom og argumentet for primitiver. For det første, med fast eiendom, dominerer beliggenhet i nesten alle situasjoner, men ytelsesgevinsten ved å bruke primitive typer kan variere sterkt fra en type applikasjon til en annen. For det andre, med fast eiendom, er det andre faktorer å vurdere, selv om de vanligvis er mindre i forhold til beliggenhet. Med primitive typer er det bare en grunn til å bruke dem - opptreden; og bare hvis applikasjonen er den typen som kan dra nytte av bruken av dem.
Primitives gir liten verdi for de fleste forretningsrelaterte applikasjoner og Internett-applikasjoner som bruker en klient-server-programmeringsmodell med en database på backend. Men ytelsen til applikasjoner som domineres av numeriske beregninger, kan ha stor nytte av bruken av primitiver.
Inkluderingen av primitiver i Java har vært en av de mer kontroversielle beslutningene om språkdesign, som det fremgår av antall artikler og foruminnlegg relatert til denne beslutningen. Simon Ritter bemerket i sin JAX London i hovedtalen i november 2011 at det ble tatt alvorlig hensyn til fjerning av primitiver i en fremtidig versjon av Java (se lysbilde 41). I denne artikkelen vil jeg kort introdusere primitiver og Java's dual-type system. Ved hjelp av kodeeksempler og enkle referanser vil jeg gjøre min sak rede for hvorfor Java-primitiver er nødvendig for visse typer applikasjoner. Jeg vil også sammenligne Java-ytelsen til Scala, C ++ og JavaScript.
Måling av programvareytelse
Programvareytelse blir vanligvis målt i tid og rom. Tid kan være faktisk kjøretid, for eksempel 3,7 minutter, eller vekstrekkefølge basert på størrelsen på innspillet, for eksempel O(n2). Lignende tiltak eksisterer for plassytelse, som ofte uttrykkes i form av hovedminnebruk, men kan også utvide til diskbruk. Forbedring av ytelsen innebærer vanligvis en avveining mellom tid og rom ved at endringer for å forbedre tiden ofte har en skadelig effekt på rommet, og omvendt. En vekstrekkefølge måling er avhengig av algoritmen, og bytte fra omslagsklasser til primitiver vil ikke endre resultatet. Men når det gjelder faktisk ytelse av tid og rom, gir bruk av primitiver i stedet for wrapper-klasser forbedringer i både tid og rom samtidig.
Primitive mot objekter
Som du sikkert allerede vet om du leser denne artikkelen, har Java et system av dobbelt type, vanligvis referert til som primitive typer og objekttyper, ofte forkortet bare som primitiver og objekter. Det er åtte primitive typer forhåndsdefinerte i Java, og navnene deres er reserverte nøkkelord. Vanlige eksempler inkluderer int
, dobbelt
, og boolsk
. I hovedsak er alle andre typer i Java, inkludert alle brukerdefinerte typer, objekttyper. (Jeg sier "i det vesentlige" fordi array-typer er litt av en hybrid, men de er mye mer som objekttyper enn primitive typer.) For hver primitiv type er det en tilsvarende wrapper-klasse som er en objekttype; eksempler inkluderer Heltall
til int
, Dobbelt
til dobbelt
, og Boolsk
til boolsk
.
Primitive typer er verdibaserte, men objekttyper er referansebaserte, og i den ligger både kraften og kilden til kontrovers av primitive typer. For å illustrere forskjellen, vurder de to erklæringene nedenfor. Den første erklæringen bruker en primitiv type og den andre bruker en wrapper-klasse.
int n1 = 100; Heltall n2 = nytt Heltall (100);
Ved å bruke autoboksing, en funksjon lagt til JDK 5, kunne jeg forkorte den andre erklæringen til ganske enkelt
Heltall n2 = 100;
men den underliggende semantikken endres ikke. Autoboxing forenkler bruken av wrapper-klasser og reduserer mengden kode en programmerer må skrive, men det endrer ingenting under kjøretiden.
Forskjellen mellom det primitive n1
og innpakningsobjektet n2
er illustrert av diagrammet i figur 1.
Variabelen n1
har et heltall, men variabelen n2
inneholder en referanse til et objekt, og det er objektet som har heltallverdien. I tillegg refererer objektet til n2
inneholder også en referanse til klasseobjektet Dobbelt
.
Problemet med primitiver
Før jeg prøver å overbevise deg om behovet for primitive typer, bør jeg erkjenne at mange ikke er enige med meg. Sherman Alpert i "Primitive typer ansett som skadelig" hevder at primitiver er skadelige fordi de blander "prosessuell semantikk til en ellers enhetlig objektorientert modell. Primitiver er ikke førsteklasses objekter, men de eksisterer likevel på et språk som først og fremst involverer første- klasseobjekter. " Primitiver og objekter (i form av wrapper-klasser) gir to måter å håndtere logisk lignende typer på, men de har veldig forskjellige underliggende semantikk. For eksempel, hvordan skal to tilfeller sammenlignes for likestilling? For primitive typer bruker man ==
operatør, men for objekter er det foretrukne valget å ringe er lik()
metode, som ikke er et alternativ for primitiver. Tilsvarende eksisterer forskjellige semantikk når man tildeler verdier eller sender parametere. Selv standardverdiene er forskjellige; f.eks. 0
til int
mot null
til Heltall
.
For mer bakgrunn om dette problemet, se Eric Brunos blogginnlegg "A modern primitive discussion", som oppsummerer noen av fordelene og ulempene ved primitiver. En rekke diskusjoner om Stack Overflow fokuserer også på primitiver, inkludert "Hvorfor bruker folk fremdeles primitive typer i Java?" og "Er det en grunn til alltid å bruke Objekter i stedet for primitiver ?." Programmører Stack Exchange er vert for en lignende diskusjon med tittelen "Når skal jeg bruke primitive vs class i Java?".
Minneutnyttelse
EN dobbelt
i Java opptar alltid 64 bits i minnet, men størrelsen på en referanse avhenger av Java virtual machine (JVM). Datamaskinen min kjører 64-biters versjonen av Windows 7 og en 64-bit JVM, og derfor har en referanse på datamaskinen min 64 bit. Basert på diagrammet i figur 1 forventer jeg en singel dobbelt
som for eksempel n1
å okkupere 8 byte (64 bits), og jeg forventer en enkelt Dobbelt
som for eksempel n2
å okkupere 24 byte - 8 for referansen til objektet, 8 for dobbelt
verdi lagret i objektet, og 8 for referansen til klasseobjektet for Dobbelt
. I tillegg bruker Java ekstra minne for å støtte søppelinnsamling for objekttyper, men ikke for primitive typer. La oss sjekke det ut.
Ved å bruke en tilnærming som ligner på Glen McCluskey i "Java primitive types vs. wrappers", måler metoden vist i liste 1 antall byte okkupert av en n-for-n-matrise (todimensjonalt array) av dobbelt
.
Oppføring 1. Beregning av minneutnyttelse av type dobbelt
offentlig statisk lang getBytesUsingPrimitives (int n) {System.gc (); // tving søppelinnsamling lang memStart = Runtime.getRuntime (). freeMemory (); dobbel [] [] a = ny dobbel [n] [n]; // sett noen tilfeldige verdier i matrisen for (int i = 0; i <n; ++ i) {for (int j = 0; j <n; ++ j) a [i] [j] = Math. tilfeldig(); } lang memEnd = Runtime.getRuntime (). freeMemory (); returner memStart - memEnd; }
Endring av koden i liste 1 med de åpenbare typeendringene (ikke vist), kan vi også måle antall byte okkupert av en n-for-n-matrise av Dobbelt
. Når jeg tester disse to metodene på datamaskinen min ved hjelp av 1000 ganger 1000 matriser, får jeg resultatene vist i tabell 1 nedenfor. Som illustrert, versjonen for primitiv type dobbelt
tilsvarer litt mer enn 8 byte per oppføring i matrisen, omtrent hva jeg forventet. Imidlertid versjonen for objekttype Dobbelt
krevde litt mer enn 28 byte per oppføring i matrisen. Dermed, i dette tilfellet, minnebruk av Dobbelt
er mer enn tre ganger minnebruk av dobbelt
, som ikke burde være en overraskelse for alle som forstår minnelayoutet illustrert i figur 1 ovenfor.
Tabell 1. Minneutnyttelse av dobbelt versus dobbelt
Versjon | Totalt antall byte | Byte per oppføring |
---|---|---|
Ved hjelp av dobbelt | 8,380,768 | 8.381 |
Ved hjelp av Dobbelt | 28,166,072 | 28.166 |
Runtime ytelse
For å sammenligne kjøretidsytelsen for primitiver og objekter, trenger vi en algoritme dominert av numeriske beregninger. For denne artikkelen har jeg valgt matrisemultiplikasjon, og jeg beregner tiden som kreves for å multiplisere to 1000 ganger 1000 matriser. Jeg kodet matrisemultiplikasjon for dobbelt
på en enkel måte som vist i liste 2 nedenfor. Selv om det kan være raskere måter å implementere matrisemultiplikasjon (kanskje ved hjelp av samtidighet), er det punktet egentlig ikke relevant for denne artikkelen. Alt jeg trenger er vanlig kode i to lignende metoder, en som bruker den primitive dobbelt
og en som bruker innpakningsklassen Dobbelt
. Koden for å multiplisere to matriser av typen Dobbelt
er akkurat slik i Listing 2 med de åpenbare typeendringene.
Oppføring 2. Multipliser to matriser av typen dobbelt
offentlig statisk dobbel [] [] multipliser (dobbel [] [] a, dobbelt [] [] b) {hvis (! checkArgs (a, b)) kaster ny IllegalArgumentException ("Matriser ikke kompatible for multiplikasjon"); int nRows = a. lengde; int nCols = b [0] .lengde; doble [] [] resultat = nye doble [nRader] [nCols]; for (int rowNum = 0; rowNum <nRows; ++ rowNum) {for (int colNum = 0; colNum <nCols; ++ colNum) {dobbel sum = 0,0; for (int i = 0; i <a [0] .length; ++ i) sum + = a [rowNum] [i] * b [i] [colNum]; resultat [rowNum] [colNum] = sum; }} returner resultat; }
Jeg kjørte de to metodene for å multiplisere to 1000 ganger 1000 matriser på datamaskinen min flere ganger og målte resultatene. Gjennomsnittstidene er vist i tabell 2. Dermed, i dette tilfellet, kjører ytelsen til dobbelt
er mer enn fire ganger så raskt som for Dobbelt
. Det er rett og slett for stor forskjell å ignorere.
Tabell 2. Runtime-ytelse av dobbelt kontra dobbelt
Versjon | Sekunder |
---|---|
Ved hjelp av dobbelt | 11.31 |
Ved hjelp av Dobbelt | 48.48 |
SciMark 2.0-referansen
Så langt har jeg brukt den enkle, enkle referansen for matriksmultiplikasjon for å demonstrere at primitiver kan gi betydelig større dataytelse enn objekter. For å styrke mine påstander vil jeg bruke en mer vitenskapelig referanse. SciMark 2.0 er et Java-standard for vitenskapelig og numerisk databehandling tilgjengelig fra National Institute of Standards and Technology (NIST). Jeg lastet ned kildekoden for denne referanseindeksen og opprettet to versjoner, den originale versjonen med primitiver og en andre versjon ved bruk av wrapper-klasser. For den andre versjonen byttet jeg ut int
med Heltall
og dobbelt
med Dobbelt
for å få full effekt av å bruke innpakningsklasser. Begge versjonene er tilgjengelige i kildekoden for denne artikkelen.
SciMark-målestokken måler ytelsen til flere beregningsrutiner og rapporterer en sammensatt score i omtrent Mflops (millioner flytende punktoperasjoner per sekund). Dermed er større tall bedre for denne referanseindeksen. Tabell 3 gir gjennomsnittlige sammensatte score fra flere kjøringer av hver versjon av denne referanseindeksen på datamaskinen min. Som vist var kjøretidsytelsene til de to versjonene av SciMark 2.0-referansen i samsvar med matrisemultiplikasjonsresultatene ovenfor ved at versjonen med primitiver var nesten fem ganger raskere enn versjonen ved bruk av wrapper-klasser.
Tabell 3. Runtime-ytelse for SciMark-referansen
SciMark-versjon | Ytelse (Mflops) |
---|---|
Bruk primitiver | 710.80 |
Bruk av emballasjeklasser | 143.73 |
Du har sett noen varianter av Java-programmer som gjør numeriske beregninger, ved å bruke både en hjemmelaget standard og en mer vitenskapelig. Men hvordan sammenligner Java seg med andre språk? Jeg vil avslutte med en rask titt på hvordan Java-ytelsen sammenlignes med tre andre programmeringsspråk: Scala, C ++ og JavaScript.
Benchmarking av Scala
Scala er et programmeringsspråk som kjører på JVM og ser ut til å bli stadig mer populært. Scala har et enhetlig typesystem, noe som betyr at det ikke skiller mellom primitiver og objekter. I følge Erik Osheim i Scalas numeriske typeklasse (Pt. 1) bruker Scala primitive typer når det er mulig, men vil bruke objekter om nødvendig. På samme måte sier Martin Oderskys beskrivelse av Scalas arrays at "... en Scala-matrise Array [Int]
er representert som en Java int []
, en Array [Double]
er representert som en Java dobbelt[]
..."
Så betyr dette at Scalas enhetlige typesystem vil ha kjøretidsytelse som kan sammenlignes med Javas primitive typer? La oss se.