Programmering

Et tilfelle for å beholde primitiver i Java

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.

John I. Moore, Jr.

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

VersjonTotalt antall byteByte per oppføring
Ved hjelp av dobbelt8,380,7688.381
Ved hjelp av Dobbelt28,166,07228.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

VersjonSekunder
Ved hjelp av dobbelt11.31
Ved hjelp av Dobbelt48.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.

last ned Benchmarking Java: Last ned kildekoden John I. Moore, Jr.

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-versjonYtelse (Mflops)
Bruk primitiver710.80
Bruk av emballasjeklasser143.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.

$config[zx-auto] not found$config[zx-overlay] not found