Programmering

Et tilfælde til opretholdelse af primitiver i Java

Primitiver har været en del af Java-programmeringssproget siden dets oprindelige udgivelse i 1996, og alligevel er de stadig en af ​​de mere kontroversielle sprogfunktioner. John Moore gør en stærk argumentation for at holde primitiver på Java-sproget ved at sammenligne enkle Java-benchmarks, både med og uden primitiver. Han sammenligner derefter Java-ydeevnen med Scala, C ++ og JavaScript i en bestemt type applikation, hvor primitiver gør en bemærkelsesværdig forskel.

Spørgsmål: Hvad er de tre vigtigste faktorer ved køb af fast ejendom?

Svar: Placering, placering, placering.

Dette gamle og ofte anvendte ordsprog er beregnet til at antyde, at placeringen dominerer fuldstændigt alle andre faktorer, når det kommer til fast ejendom. I et lignende argument er de tre vigtigste faktorer, der skal overvejes for at bruge primitive typer i Java, performance, performance, performance. Der er to forskelle mellem argumentet for fast ejendom og argumentet for primitiver. For det første dominerer placeringen med fast ejendom i næsten alle situationer, men præstationsgevinsterne ved at bruge primitive typer kan variere meget fra en slags applikation til en anden. For det andet med fast ejendom er der andre faktorer at overveje, selvom de normalt er mindre i forhold til placering. Med primitive typer er der kun en grund til at bruge dem - ydeevne; og kun hvis applikationen er den slags, der kan drage fordel af deres anvendelse.

Primitiver tilbyder ringe værdi til de fleste forretningsrelaterede applikationer og internetapplikationer, der bruger en klientserver-programmeringsmodel med en database på backend. Men udførelsen af ​​applikationer, der er domineret af numeriske beregninger, kan have stor gavn af brugen af ​​primitiver.

Inkluderingen af ​​primitiver i Java har været en af ​​de mere kontroversielle beslutninger om sprogdesign, hvilket fremgår af antallet af artikler og forumindlæg relateret til denne beslutning. Simon Ritter bemærkede i sin JAX London i hovedtalen i november 2011, at der blev taget alvorligt hensyn til fjernelse af primitiver i en fremtidig version af Java (se dias 41). I denne artikel introducerer jeg kort primitiver og Java's dual-type system. Ved hjælp af kodeeksempler og enkle benchmarks vil jeg argumentere for, hvorfor Java-primitiver er nødvendige for visse typer applikationer. Jeg vil også sammenligne Java's ydeevne med Scala, C ++ og JavaScript.

Måling af softwareydelse

Softwareydelse måles normalt med hensyn til tid og rum. Tid kan være faktisk kørselstid, såsom 3,7 minutter, eller rækkefølge for vækst baseret på inputstørrelse, f.eks O(n2). Lignende foranstaltninger findes for pladsydelse, som ofte udtrykkes i form af hovedhukommelsesforbrug, men også kan udvides til diskforbrug. Forbedring af ydeevne indebærer normalt en tid-rum-kompromis, idet ændringer for at forbedre tiden ofte har en skadelig virkning på rummet og omvendt. En måling af vækstorden er afhængig af algoritmen, og skift fra indpakningsklasser til primitiver ændrer ikke resultatet. Men når det kommer til faktisk tids- og rumydelse, tilbyder brugen af ​​primitiver i stedet for indpakningsklasser forbedringer i både tid og rum samtidigt.

Primitiver versus objekter

Som du sikkert allerede ved, hvis du læser denne artikel, har Java et system af dobbelt type, normalt kaldet primitive typer og objekttyper, ofte forkortet simpelthen som primitiver og objekter. Der er otte primitive typer foruddefineret i Java, og deres navne er reserverede nøgleord. Almindeligt anvendte eksempler inkluderer int, dobbeltog boolsk. I det væsentlige er alle andre typer i Java, inklusive alle brugerdefinerede typer, objekttyper. (Jeg siger "i det væsentlige", fordi arraytyper er lidt af en hybrid, men de ligner meget mere objekttyper end primitive typer.) For hver primitiv type er der en tilsvarende indpakningsklasse, der er en objekttype; eksempler inkluderer Heltal til int, Dobbelt til dobbeltog Boolsk til boolsk.

Primitive typer er værdibaserede, men objekttyper er referencebaserede, og deri ligger både magt og kilde til kontrovers af primitive typer. For at illustrere forskellen skal du overveje de to erklæringer nedenfor. Den første erklæring bruger en primitiv type, og den anden bruger en indpakningsklasse.

 int n1 = 100; Heltal n2 = nyt heltal (100); 

Ved hjælp af autoboxing, en funktion tilføjet til JDK 5, kunne jeg forkorte den anden erklæring til simpelthen

 Heltal n2 = 100; 

men den underliggende semantik ændrer sig ikke. Autoboxing forenkler brugen af ​​indpakningsklasser og reducerer mængden af ​​kode, som en programmør skal skrive, men det ændrer ikke noget under kørsel.

Forskellen mellem det primitive n1 og indpakningsobjektet n2 er illustreret med diagrammet i figur 1.

John I. Moore, Jr.

Variablen n1 har et heltal, men variablen n2 indeholder en henvisning til et objekt, og det er objektet, der indeholder heltalets værdi. Derudover refererer objektet til n2 indeholder også en henvisning til klasseobjektet Dobbelt.

Problemet med primitiver

Før jeg prøver at overbevise dig om behovet for primitive typer, skal jeg erkende, at mange mennesker ikke er enige med mig. Sherman Alpert i "Primitive typer betragtes som skadelig" argumenterer for, at primitiver er skadelige, fordi de blander "proceduremessantik til en ellers ensartet objektorienteret model. Primitiver er ikke førsteklasses objekter, alligevel findes de på et sprog, der primært involverer første- klasseobjekter. " Primitiver og objekter (i form af indpakningsklasser) giver to måder at håndtere logisk lignende typer på, men de har meget forskellige underliggende semantik. For eksempel, hvordan skal to tilfælde sammenlignes med henblik på lighed? For primitive typer bruger man == operatør, men for objekter er det foretrukne valg at kalde lige med() metode, som ikke er en mulighed for primitiver. Tilsvarende findes der forskellige semantikker, når man tildeler værdier eller videregiver parametre. Selv standardværdierne er forskellige; f.eks., 0 til int imod nul til Heltal.

For mere baggrund om dette emne, se Eric Brunos blogindlæg, "En moderne primitiv diskussion", der opsummerer nogle af fordele og ulemper ved primitiver. En række diskussioner om Stack Overflow fokuserer også på primitiver, herunder "Hvorfor bruger folk stadig primitive typer i Java?" og "Er der en grund til altid at bruge objekter i stedet for primitiver?" Programmører Stack Exchange er vært for en lignende diskussion med titlen "Hvornår skal man bruge primitive vs class i Java?".

Hukommelsesudnyttelse

EN dobbelt i Java optager altid 64 bit i hukommelsen, men størrelsen på en reference afhænger af den virtuelle Java-maskine (JVM). Min computer kører 64-bit versionen af ​​Windows 7 og en 64-bit JVM, og derfor har en reference på min computer 64 bit. Baseret på diagrammet i figur 1 forventer jeg en enkelt dobbelt såsom n1 at besætte 8 byte (64 bit), og jeg forventer en enkelt Dobbelt såsom n2 at besætte 24 byte - 8 for henvisningen til objektet, 8 for dobbelt værdi, der er gemt i objektet, og 8 som reference til klasseobjektet for Dobbelt. Plus, Java bruger ekstra hukommelse til at understøtte affaldssamling til objekttyper, men ikke til primitive typer. Lad os tjekke det ud.

Ved at bruge en fremgangsmåde svarende til Glen McCluskey i "Java primitive typer vs. indpakninger" måler metoden vist i liste 1 antallet af bytes, der er optaget af en n-for-n-matrix dobbelt.

Liste 1. Beregning af hukommelsesudnyttelse af typen dobbelt

 offentlig statisk lang getBytesUsingPrimitives (int n) {System.gc (); // tvinge affaldssamling lang memStart = Runtime.getRuntime (). freeMemory (); dobbelt [] [] a = ny dobbelt [n] [n]; // sæt nogle tilfældige værdier i matrixen for (int i = 0; i <n; ++ i) {for (int j = 0; j <n; ++ j) a [i] [j] = Math. tilfældig(); } lang memEnd = Runtime.getRuntime (). freeMemory (); returner memStart - memEnd; } 

Ændring af koden i liste 1 med de åbenlyse typeændringer (ikke vist), vi kan også måle antallet af bytes optaget af en n-for-n-matrix af Dobbelt. Når jeg tester disse to metoder på min computer ved hjælp af 1000 gange 1000 matricer, får jeg resultaterne vist i tabel 1 nedenfor. Som illustreret, versionen til primitiv type dobbelt svarer til lidt mere end 8 byte pr. post i matrixen, omtrent hvad jeg forventede. Dog versionen til objekttype Dobbelt krævede lidt mere end 28 byte pr. post i matrixen. I dette tilfælde er hukommelsesudnyttelsen af Dobbelt er mere end tre gange hukommelsesudnyttelsen af dobbelt, som ikke burde være en overraskelse for nogen, der forstår hukommelseslayoutet, der er illustreret i figur 1 ovenfor.

Tabel 1. Hukommelsesudnyttelse af dobbelt versus dobbelt

VersionI alt byteBytes pr. Post
Ved brug af dobbelt8,380,7688.381
Ved brug af Dobbelt28,166,07228.166

Kørselstid

For at sammenligne runtime-præstationer for primitiver og objekter har vi brug for en algoritme domineret af numeriske beregninger. Til denne artikel har jeg valgt matrixmultiplikation, og jeg beregner den nødvendige tid til at multiplicere to 1000-til-1000 matricer. Jeg kodede matrixmultiplikation for dobbelt på en ligetil måde som vist i liste 2 nedenfor. Selvom der kan være hurtigere måder at implementere matrixmultiplikation (måske ved hjælp af samtidighed), er dette punkt ikke rigtig relevant for denne artikel. Alt, hvad jeg har brug for, er fælles kode i to lignende metoder, den ene ved hjælp af den primitive dobbelt og en ved hjælp af indpakningsklassen Dobbelt. Koden til multiplikation af to matricer af typen Dobbelt er nøjagtigt sådan i Listing 2 med de åbenlyse typeændringer.

Liste 2. Multiplikation af to matricer af typen dobbelt

 offentlig statisk dobbelt [] [] gang (dobbelt [] [] a, dobbelt [] [] b) {hvis (! checkArgs (a, b)) smider nyt IllegalArgumentException ("Matricer ikke kompatible til multiplikation"); int nRows = a. længde; int nCols = b [0]. længde; dobbelt [] [] resultat = nyt dobbelt [nRækker] [nCols]; for (int rowNum = 0; rowNum <nRows; ++ rowNum) {for (int colNum = 0; colNum <nCols; ++ colNum) {dobbelt sum = 0,0; for (int i = 0; i <a [0]. længde; ++ i) sum + = a [rowNum] [i] * b [i] [colNum]; resultat [rowNum] [colNum] = sum; }} returner resultat; } 

Jeg kørte de to metoder til at multiplicere to 1000-til-1000 matricer på min computer flere gange og målte resultaterne. Gennemsnitstiderne er vist i tabel 2. I dette tilfælde er runtime-ydeevnen således dobbelt er mere end fire gange så hurtig som for Dobbelt. Det er simpelthen for meget forskel til at ignorere.

Tabel 2. Runtime-ydeevne af dobbelt versus dobbelt

VersionSekunder
Ved brug af dobbelt11.31
Ved brug af Dobbelt48.48

SciMark 2.0 benchmark

Indtil videre har jeg brugt det enkle, enkle benchmark for matrixmultiplikation for at demonstrere, at primitive kan give betydeligt større computerydelse end objekter. For at forstærke mine påstande bruger jeg et mere videnskabeligt benchmark. SciMark 2.0 er et Java-benchmark for videnskabelig og numerisk computing, der fås fra National Institute of Standards and Technology (NIST). Jeg downloadede kildekoden til dette benchmark og oprettede to versioner, den originale version ved hjælp af primitiver og en anden version ved hjælp af wrapper-klasser. For den anden version erstattede jeg int med Heltal og dobbelt med Dobbelt for at få den fulde effekt af brugen af ​​indpakningsklasser. Begge versioner er tilgængelige i kildekoden til denne artikel.

download Benchmarking Java: Download kildekoden John I. Moore, Jr.

SciMark-benchmarket måler ydeevnen for flere beregningsrutiner og rapporterer en sammensat score i omtrentlige Mflops (millioner af flydende punktoperationer pr. Sekund). Således er større tal bedre for dette benchmark. Tabel 3 viser de gennemsnitlige sammensatte score fra flere kørsler af hver version af dette benchmark på min computer. Som vist var runtime-forestillingerne for de to versioner af SciMark 2.0-benchmarken i overensstemmelse med matrixmultiplikationsresultaterne ovenfor, idet versionen med primitiver var næsten fem gange hurtigere end versionen ved hjælp af indpakningsklasser.

Tabel 3. Runtime-ydeevne for SciMark-benchmarket

SciMark versionYdeevne (Mflops)
Brug af primitiver710.80
Brug af indpakningsklasser143.73

Du har set et par variationer af Java-programmer, der foretager numeriske beregninger ved hjælp af både et hjemmelavet benchmark og et mere videnskabeligt. Men hvordan sammenlignes Java med andre sprog? Jeg vil afslutte med et hurtigt kig på, hvordan Java's ydeevne kan sammenlignes med tre andre programmeringssprog: Scala, C ++ og JavaScript.

Benchmarking af Scala

Scala er et programmeringssprog, der kører på JVM og ser ud til at vinde popularitet. Scala har et samlet typesystem, hvilket betyder, at det ikke skelner mellem primitiver og objekter. Ifølge Erik Osheim i Scalas numeriske typeklasse (Pt. 1) bruger Scala primitive typer, når det er muligt, men bruger om nødvendigt objekter. På samme måde siger Martin Oderskys beskrivelse af Scala's Arrays, at "... en Scala-array Array [Int] er repræsenteret som en Java int [], en Array [dobbelt] er repræsenteret som en Java dobbelt[] ..."

Så betyder det, at Scalas samlede typesystem vil have runtime-ydeevne, der kan sammenlignes med Java's primitive typer? Lad os se.