Ett fall för att hålla primitiver i Java

primitiver har varit en del av Java-programmeringsspråket sedan den första utgåvan 1996, och ändå är de fortfarande en av de mer kontroversiella språkfunktionerna. John Moore gör ett starkt fall för att hålla primitiver i Java-språket genom att jämföra enkla Java-riktmärken, både med och utan primitiver. Han jämför sedan Java-prestanda med Scala, C++ och JavaScript i en viss typ av applikation, där primitiver gör en anmärkningsvärd skillnad.

fråga: Vilka är de tre viktigaste faktorerna för att köpa fastigheter?
svar: Plats, Plats, Plats.

detta gamla och ofta använda ordspråk är tänkt att antyda att platsen helt dominerar alla andra faktorer när det gäller fastigheter. I ett liknande argument är de tre viktigaste faktorerna att tänka på för att använda primitiva typer i Java prestanda, prestanda, prestanda. Det finns två skillnader mellan argumentet för fastigheter och argumentet för primitiva. För det första med fastigheter dominerar platsen i nästan alla situationer, men prestationsvinsterna från att använda primitiva typer kan variera mycket från en typ av applikation till en annan. Andra, med fastigheter, det finns andra faktorer att tänka på även om de är oftast mindre i jämförelse med plats. Med primitiva typer finns det bara en anledning att använda dem — prestanda; och då bara om applikationen är den typ som kan dra nytta av deras användning.

primitiver erbjuder lite värde för de flesta affärsrelaterade och internetapplikationer som använder en klient-serverprogrammeringsmodell med en databas på backend. Men prestanda för applikationer som domineras av numeriska beräkningar kan dra stor nytta av användningen av primitiver.

införandet av primitiver i Java har varit ett av de mer kontroversiella språkdesignbesluten, vilket framgår av antalet artiklar och foruminlägg relaterade till detta beslut. Simon Ritter noterade i sin Jax London i November 2011 huvudtal att man allvarligt övervägde att ta bort primitiva i en framtida version av Java (SE bild 41). I den här artikeln ska jag kort presentera primitives och Java dual-type system. Med hjälp av kodexempel och enkla riktmärken kommer jag att göra mitt fall för varför Java primitiver behövs för vissa typer av applikationer. Jag kommer också att jämföra Java: s prestanda med Scala, C++ och JavaScript.

primitiver kontra objekt

som du förmodligen redan vet om du läser den här artikeln har Java ett dubbeltypssystem, vanligtvis kallat primitiva typer och objekttyper, ofta förkortat helt enkelt som primitiver och objekt. Det finns åtta primitiva typer fördefinierade i Java, och deras namn är reserverade nyckelord. Vanliga exempel inkluderar int, double och boolean. I huvudsak är alla andra typer i Java, inklusive alla användardefinierade typer, objekttyper. (Jag säger ”väsentligen” eftersom arraytyper är lite av en hybrid, men de är mycket mer som objekttyper än primitiva typer.) För varje primitiv typ finns en motsvarande omslagsklass som är en objekttyp; exempel inkluderar Integer för int, Double för double och Booleanför boolean.

primitiva typer är värdebaserade, men objekttyper är referensbaserade, och däri ligger både kraften och källan till kontroverser för primitiva typer. För att illustrera skillnaden, överväga de två deklarationerna nedan. Den första deklarationen använder en primitiv typ och den andra använder en omslagsklass.

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

med hjälp av autoboxing, en funktion som läggs till JDK 5, kunde jag förkorta den andra deklarationen till helt enkelt

Integer n2 = 100;

men den underliggande semantiken ändras inte. Autoboxing förenklar användningen av omslagsklasser och minskar mängden kod som en programmerare måste skriva, men det ändrar ingenting vid körning.

skillnaden mellan den primitiva n1 och omslagsobjektet n2 illustreras av diagrammet i Figur 1.

John I. Moore, Jr.

Figur 1. Minneslayout av primitiver kontra objekt

variabeln n1 har ett heltalsvärde, men variabeln n2 innehåller en referens till ett objekt, och det är objektet som innehåller heltalsvärdet. Dessutom innehåller objektet som refereras av n2 också en hänvisning till klassobjektet Double.

problemet med primitiver

innan jag försöker övertyga dig om behovet av primitiva typer, bör jag erkänna att många människor inte håller med mig. Sherman Alpert i” primitiva typer anses skadliga ” hävdar att primitiva är skadliga eftersom de blandar ”procedurell semantik i en annars enhetlig objektorienterad modell. Primitiver är inte förstklassiga objekt, men de finns på ett språk som i första hand involverar förstklassiga objekt.”Primitiver och föremål (i form av omslagsklasser) ger två sätt att hantera logiskt liknande typer, men de har mycket olika underliggande semantik. Till exempel, hur ska två instanser jämföras för jämlikhet? För primitiva typer använder man == – operatören, men för objekt är det föredragna valet att ringa equals() – metoden, vilket inte är ett alternativ för primitiver. På samma sätt finns olika semantik när man tilldelar värden eller passerar parametrar. Även standardvärdena är olika; t.ex. 0 för int kontra null för Integer.

för mer bakgrund i denna fråga, se Eric Brunos blogginlägg,” a modern primitive discussion”, som sammanfattar några av fördelarna och nackdelarna med primitiva. Ett antal diskussioner om Stack Overflow fokuserar också på primitiver, inklusive ”Varför använder människor fortfarande primitiva typer i Java?”och” finns det en anledning att alltid använda objekt istället för primitiva?.”Programmerare Stack Exchange är värd för en liknande diskussion med titeln” När ska man använda primitiv vs-klass i Java?”.

minnesanvändning

a double i Java upptar alltid 64 bitar i minnet, men storleken på en referens beror på Java virtual machine (JVM). Min dator kör 64-bitarsversionen av Windows 7 och en 64-bitars JVM, och därför upptar en referens på min dator 64 bitar. Baserat på diagrammet i Figur 1 förväntar jag mig en enda double som n1 att uppta 8 byte (64 bitar), och jag förväntar mig en enda Double som n2 att uppta 24 byte — 8 för referensen till objektet, 8 för double – värdet lagrat i objektet och 8 för referensen till klassobjektet för Double. Dessutom använder Java extra minne för att stödja sophämtning för objekttyper men inte för primitiva typer. Låt oss kolla upp det.

använda ett tillvägagångssätt som liknar Glen McCluskey i ” Java primitive types vs. wrappers, ” metoden som visas i Lista 1 mäter antalet byte som upptas av en n-by-n-matris (tvådimensionell array) på double.

Lista 1. Beräkning av minnesanvändning av typ dubbel

ändra koden i Lista 1 med de uppenbara typändringarna (visas inte), vi kan också mäta antalet byte som upptas av en n-by-n-matris av Double. När jag testar dessa två metoder på min dator med 1000-by-1000 matriser får jag resultaten som visas i Tabell 1 nedan. Som illustrerat motsvarar versionen för primitiv typ double lite mer än 8 byte per post i matrisen, ungefär vad jag förväntade mig. Versionen för objekttyp Double krävde dock lite mer än 28 byte per post i matrisen. I detta fall är minnesutnyttjandet av Double mer än tre gånger minnesutnyttjandet av double, vilket inte borde vara en överraskning för alla som förstår minneslayouten som illustreras i Figur 1 ovan.

Runtime performance

för att jämföra runtime-prestanda för primitiva och objekt behöver vi en algoritm som domineras av numeriska beräkningar. För den här artikeln har jag valt matrismultiplikation, och jag beräknar den tid som krävs för att multiplicera två 1000-by-1000 matriser. Jag kodade matrismultiplikation för double på ett enkelt sätt som visas i Lista 2 nedan. Även om det kan finnas snabbare sätt att implementera matrismultiplikation (kanske med samtidighet), är den punkten inte riktigt relevant för den här artikeln. Allt jag behöver är vanlig kod i två liknande metoder, en med primitiv double och en med wrapper-klassen Double. Koden för att multiplicera två matriser av typen Double är exakt som i Listning 2 med de uppenbara typändringarna.

Lista 2. Multiplicera två matriser av typen double

jag sprang de två metoderna för att multiplicera två 1000-by-1000 matriser på min dator flera gånger och mätte resultaten. De genomsnittliga tiderna visas i Tabell 2. I detta fall är körtidsprestandan för doublemer än fyra gånger så snabb som för Double. Det är helt enkelt för mycket av en skillnad att ignorera.

SciMark 2.0-riktmärket

hittills har jag använt det enkla, enkla riktmärket för matrismultiplikation för att visa att primitiver kan ge betydligt större datorprestanda än objekt. För att förstärka mina påståenden använder jag ett mer vetenskapligt riktmärke. SciMark 2.0 är ett Java-riktmärke för vetenskaplig och numerisk databehandling tillgänglig från National Institute of Standards and Technology (NIST). Jag laddade ner källkoden för detta riktmärke och skapade två versioner, den ursprungliga versionen med primitiver och en andra version med wrapper-klasser. För den andra versionen ersatte jag int med Integer och double med Double för att få full effekt av att använda wrapper-klasser. Båda versionerna finns i källkoden för den här artikeln.

ladda ner

John I. Moore, Jr.

SciMark benchmark mäter prestanda för flera beräkningsrutiner och rapporterar en sammansatt poäng i ungefärliga Mflops (miljoner flytpunktsoperationer per sekund). Således är större antal bättre för detta riktmärke. Tabell 3 ger de genomsnittliga sammansatta poängen från flera körningar av varje version av detta riktmärke på min dator. Som visat var runtime-prestationerna för de två versionerna av SciMark 2.0-riktmärket förenliga med matrismultiplikationsresultaten ovan genom att versionen med primitiver var nästan fem gånger snabbare än versionen med wrapper-klasser.

du har sett några variationer av Java-program som gör numeriska beräkningar, med både ett hemodlat riktmärke och ett mer vetenskapligt. Men hur jämför Java med andra språk? Jag avslutar med en snabb titt på hur Java: s prestanda jämförs med tre andra programmeringsspråk: Scala, C++ och JavaScript.

Benchmarking Scala

Scala är ett programmeringsspråk som körs på JVM och verkar bli allt populärare. Scala har ett enhetligt typsystem, vilket innebär att det inte skiljer mellan primitiver och objekt. Enligt Erik Osheim i Scalas numeriska typklass (Pt. 1), Scala använder primitiva typer när det är möjligt men kommer att använda objekt om det behövs. På samma sätt säger Martin Oderskys beskrivning av Scalas matriser att ”… en Scala array Array representeras som en Java int, en Array representeras som en Java double…”

så betyder det att Scalas unified type-system kommer att ha runtime-prestanda jämförbar med Java: s primitiva typer? Låt oss se.

jag är inte lika skicklig med Scala som jag är med Java, men jag försökte konvertera koden för matrix multiplikation benchmark direkt från Java till Scala. Resultatet visas i Lista 3 nedan. När jag körde Scala-versionen av riktmärket på min dator var det i genomsnitt 12,30 sekunder, vilket sätter Scala-prestanda mycket nära Java med primitiver. Det resultatet är mycket bättre än jag förväntade mig och stöder påståenden om hur Scala hanterar numeriska typer.

ladda ner

John I. Moore, Jr.

Lista 3. Multiplicera två matriser i Scala

Benchmarking C++

eftersom C++ körs direkt på ”bare metal” snarare än i en virtuell maskin, skulle man naturligtvis förvänta sig att C++ körs snabbare än Java. Dessutom reduceras Java-prestanda något av det faktum att Java kontrollerar åtkomst till arrayer för att säkerställa att varje index ligger inom de gränser som deklareras för arrayen, medan C++ inte gör det (en C++ – funktion som kan leda till buffertspill, som kan utnyttjas av hackare). Jag tyckte att C++ var något mer besvärligt än Java när det gäller att hantera grundläggande tvådimensionella arrayer, men lyckligtvis kan mycket av denna besvärlighet döljas inuti de privata delarna av en klass. För C++ skapade jag en enkel version av en Matrix – klass, och jag överbelastade operatören * för att multiplicera två matriser, men den grundläggande matrismultiplikationsalgoritmen konverterades direkt från Java-versionen. C++ – källkoden visas i Lista 4.

ladda ner

John I. Moore, Jr.

lista 4. Multiplicera två matriser i C++

med Eclipse CDT (Eclipse för C++ – utvecklare) med MinGW C++ – kompilatorn är det möjligt att skapa både debug-och release-versioner av en applikation. För att testa C++ körde jag release-versionen flera gånger och genomsnittliga resultaten. Som förväntat sprang C++ märkbart snabbare på detta enkla riktmärke, i genomsnitt 7,58 sekunder på min dator. Om raw-prestanda är den primära faktorn för att välja ett programmeringsspråk, är C++ det språk du väljer för numeriskt intensiva applikationer.

Benchmarking JavaScript

Okej, den här förvånade mig. Med tanke på att JavaScript är ett mycket dynamiskt språk, förväntade jag mig att dess prestanda skulle vara värst av allt, ännu värre än Java med wrapper-klasser. Men i själva verket var JavaScript prestanda mycket närmare Java med primitiver. För att testa JavaScript installerade jag Node.js, en JavaScript-motor med rykte om att vara mycket effektiv. Resultaten var i genomsnitt 15,91 sekunder. Lista 5 visar JavaScript-versionen av matrix multiplikation benchmark som jag körde på Node.js

ladda ner

John I. Moore, Jr.

Lista 5. Multiplicera två matriser i JavaScript

Sammanfattningsvis

när Java först kom till scenen för 18 år sedan var det inte det bästa språket ur ett prestationsperspektiv för applikationer som domineras av numeriska beräkningar. Men med tiden, med tekniska framsteg inom områden som just-in-time (JIT) – kompilering (aka adaptiv eller dynamisk kompilering), är Java: s prestanda för dessa typer av applikationer nu jämförbar med den för språk som sammanställs till inbyggd kod när primitiva typer används.

dessutom eliminerar primitiver behovet av sophämtning, vilket ger en annan prestandafördel för primitiver över objekttyper. Tabell 4 sammanfattar körtidsprestanda för matrix multiplikation riktmärke på min dator. Andra faktorer som underhåll, bärbarhet och utvecklarexpertis gör Java till ett bättre val för många sådana applikationer.

som tidigare diskuterats verkar Oracle allvarligt överväga borttagandet av primitiver i en framtida version av Java. Om inte Java-kompilatorn kan generera kod med prestanda jämförbar med primitives, tror jag att deras borttagning från Java skulle utesluta användningen av Java för vissa klasser av applikationer; nämligen de applikationer som domineras av numeriska beräkningar. I den här artikeln har jag använt ett enkelt riktmärke baserat på matrismultiplikation och ett mer vetenskapligt riktmärke, SciMark 2.0, för att argumentera för denna punkt.

om författaren

John I. Moore, Jr., Professor i matematik och datavetenskap vid citadellet, har en bred erfarenhet inom både industri och akademi, med särskild kompetens inom områdena objektorienterad teknik, programvaruteknik och tillämpad matematik. I mer än tre decennier har han designat och utvecklat programvara med hjälp av relationsdatabaser och flera högordnade språk, och han har arbetat mycket i Java sedan version 1.1. Dessutom har han utvecklat och undervisat många akademiska kurser och industriella seminarier om avancerade ämnen inom datavetenskap.

vidare läsning

  1. Paul Krill skrev om Oracles långdistansplaner för Java i ”Oracle lägger ut långsiktiga Java-avsikter” (JavaWorld, mars 2012). Denna artikel, tillsammans med tillhörande kommentarer tråd, motiverade mig att skriva detta försvar av primitiver.
  2. Szymon Guz skriver om sina resultat i benchmarking primitiva typer och wrapper klasser i ”primitiver och objekt benchmark i Java” (SimonOnSoftware, januari 2011).
  3. på supportwebbplatsen för programmering-principer och övning med C++ (Addison-Wesley, 2009) tillhandahåller C++ – skaparen Bjarne Stroustrup en implementering för en matrisklass som är mycket mer komplett än den som följer med den här artikeln.
  4. John Rose, Brian Goetz och Guy Steele diskuterar ett koncept som kallas värdetyper i ”State of the Values” (OpenJDK.net, April 2014). Värdetyper kan betraktas som oföränderliga användardefinierade aggregattyper utan identitet, genom att kombinera egenskaper hos både objekt och primitiver. Mantra för värdetyper är ” koder som en klass, fungerar som en int.”

Lämna ett svar

Din e-postadress kommer inte publiceras.