Een pleidooi voor het houden van Primitieven in Java

primitieven zijn onderdeel van de programmeertaal Java sinds de eerste release in 1996, en toch blijven ze een van de meer controversiële taal features. John Moore pleit sterk voor het behouden van Primitieven in de Java-taal door eenvoudige Java-benchmarks te vergelijken, zowel met als zonder primitieven. Vervolgens vergelijkt hij de prestaties van Java met die van Scala, C++ en JavaScript in een bepaald type applicatie, waar primitieven een opmerkelijk verschil maken.

Vraag: Wat zijn de drie belangrijkste factoren bij de aankoop van onroerend goed?
antwoord: Locatie, Locatie, locatie.

dit oude en vaak gebruikte adagium is bedoeld om te impliceren dat Locatie alle andere factoren volledig domineert als het gaat om onroerend goed. In een soortgelijk argument, de drie belangrijkste factoren te overwegen voor het gebruik van primitieve types in Java zijn prestaties, prestaties, prestaties. Er zijn twee verschillen tussen het argument voor onroerend goed en het argument voor primitieven. Ten eerste, met onroerend goed, locatie domineert in bijna alle situaties, maar de prestatiewinst van het gebruik van primitieve types kan sterk variëren van de ene soort toepassing naar de andere. Ten tweede, met onroerend goed, zijn er andere factoren om rekening mee te houden, ook al zijn ze meestal klein in vergelijking met de locatie. Bij primitieve types is er maar één reden om ze te gebruiken — prestaties; en dan alleen als de toepassing het soort is dat kan profiteren van het gebruik ervan.

Primitieven bieden weinig waarde aan de meeste zakelijke en Internet applicaties die gebruik maken van een client-server programmeermodel met een database op de backend. Maar de prestaties van toepassingen die worden gedomineerd door numerieke berekeningen kan sterk profiteren van het gebruik van primitieven.

het opnemen van Primitieven in Java is een van de meer controversiële beslissingen op het gebied van taalontwerp, zoals blijkt uit het aantal artikelen en forumposts met betrekking tot Dit besluit. Simon Ritter merkte in zijn keynote-toespraak van JAX London In November 2011 op dat er serieus wordt nagedacht over het verwijderen van Primitieven in een toekomstige versie van Java (zie slide 41). In dit artikel zal ik kort Primitieven en Java ‘ s dual-type systeem introduceren. Met behulp van code monsters en eenvoudige benchmarks, Ik zal mijn pleidooi voor waarom Java primitieven nodig zijn voor bepaalde soorten toepassingen. Ik zal ook Java ‘ s prestaties vergelijken met die van Scala, C++ en JavaScript.

Primitieven versus objecten

zoals u waarschijnlijk al weet als u dit artikel leest, heeft Java een dual-type systeem, meestal aangeduid als Primitieven en objecttypes, vaak afgekort als Primitieven en objecten. Er zijn acht primitieve types voorgedefinieerd in Java, en hun namen zijn gereserveerde zoekwoorden. Veelgebruikte voorbeelden zijn int, double en boolean. In wezen zijn alle andere typen in Java, inclusief alle door de gebruiker gedefinieerde typen, objecttypen. (Ik zeg “in wezen” omdat array types een beetje een hybride zijn, maar ze zijn veel meer als objecttypes dan primitieve types.) Voor elk primitief type is er een corresponderende wrapper class die een objecttype is; voorbeelden zijn Integer voor int, Double voor double, en Boolean voor boolean.

primitieve typen zijn waardegebaseerd, maar objecttypes zijn referentiegebaseerd, en daarin ligt zowel de kracht als de bron van controverse van primitieve typen. Om het verschil te illustreren, bekijk de twee verklaringen hieronder. De eerste declaratie gebruikt een primitief type en de tweede gebruikt een wrapper class.

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

met behulp van autoboxing, een functie toegevoegd aan JDK 5, Kon ik de tweede declaratie inkorten tot gewoon

Integer n2 = 100;

maar de onderliggende semantiek verandert niet. Autoboxing vereenvoudigt het gebruik van wrapper klassen en vermindert de hoeveelheid code die een programmeur moet schrijven, maar het verandert niets tijdens runtime.

het verschil tussen de primitieve n1 en het wrapper-object n2 wordt geïllustreerd door het diagram in Figuur 1.

John I. Moore, Jr.

Figuur 1. Geheugen lay-out van primitieven versus objecten

de variabele n1 heeft een gehele waarde, maar de variabele n2 bevat een verwijzing naar een object, en het is het object dat de gehele waarde heeft. Bovendien bevat het object waarnaar wordt verwezen door n2 ook een verwijzing naar het klasse-object Double.

het probleem met primitieven

voordat ik u probeer te overtuigen van de noodzaak van primitieve types, moet ik erkennen dat veel mensen het niet met me eens zullen zijn. Sherman Alpert in” Primitive types considered harmful “stelt dat primitieven schadelijk zijn omdat ze” procedurele semantiek mengen in een verder uniform object-georiënteerd model. Primitieven zijn geen eersteklas objecten, maar ze bestaan in een taal die voornamelijk eersteklas objecten omvat.”Primitieven en objecten (in de vorm van wrapper classes) bieden twee manieren om logisch vergelijkbare types te hanteren, maar ze hebben zeer verschillende onderliggende semantiek. Bijvoorbeeld, hoe moeten twee gevallen worden vergeleken voor gelijkheid? Voor primitieve types gebruikt men de == operator, maar voor objecten wordt de voorkeur gegeven aan de equals() methode, wat geen optie is voor primitieven. Op dezelfde manier bestaan er verschillende semantiek bij het toewijzen van waarden of het doorgeven van parameters. Zelfs de standaardwaarden zijn verschillend; bijvoorbeeld 0 voor int versus null voor Integer.

voor meer achtergrondinformatie over dit onderwerp, zie Eric Bruno ‘ s blog post, “a modern primitive discussion,” die een samenvatting geeft van enkele van de voors en tegens van primitieven. Een aantal discussies over Stack Overflow richten zich ook op primitieven, waaronder ” waarom gebruiken mensen nog steeds primitieve types in Java?”en” Is er een reden om altijd objecten te gebruiken in plaats van primitieven?.”Programmers Stack Exchange hosts een soortgelijke discussie getiteld” wanneer te gebruiken primitieve vs klasse in Java?”.

geheugengebruik

a double in Java bezet altijd 64 bits in geheugen, maar de grootte van een referentie hangt af van de Java virtual machine (JVM). Mijn computer draait de 64-bits versie van Windows 7 en een 64-bits JVM, en daarom een referentie op mijn computer bezet 64-bits. Op basis van het diagram in Figuur 1 zou ik verwachten dat een enkele double zoals n1 8 bytes (64 bits) in beslag neemt, en ik zou verwachten dat een enkele Double zoals n2 24 bytes — 8 in beslag neemt voor de verwijzing naar het object, 8 voor de double waarde opgeslagen in het object, en 8 voor de verwijzing naar het class object voor Double. Bovendien gebruikt Java extra geheugen om vuilnis te verzamelen voor objecttypen, maar niet voor primitieve types te ondersteunen. Laten we gaan kijken.

met behulp van een aanpak vergelijkbaar met die van Glen McCluskey in ” Java primitive types vs. wrappers, ” de methode in Lijst 1 Meet het aantal bytes bezet door een n-bij-n matrix (tweedimensionale array) van double.

Lijst 1. Het berekenen van geheugengebruik van type double

het wijzigen van de code in Lijst 1 met de voor de hand liggende typewijzigingen (niet weergegeven), kunnen we ook het aantal bytes meten dat wordt ingenomen door een n-by-n matrix van Double. Als ik deze twee methoden op mijn computer test met behulp van 1000-bij-1000 matrices, krijg ik de resultaten weergegeven in Tabel 1 hieronder. Zoals geà llustreerd, komt de versie voor primitief type double overeen met iets meer dan 8 bytes per entry in de matrix, ongeveer wat ik verwachtte. Echter, de versie voor objecttype Double vereiste iets meer dan 28 bytes per item in de matrix. In dit geval is het geheugengebruik van Double dus meer dan driemaal het geheugengebruik van double, wat geen verrassing zou moeten zijn voor iedereen die de in Figuur 1 getoonde geheugenindeling begrijpt.

runtime performance

om de runtime performance voor Primitieven en objecten te vergelijken, hebben we een algoritme nodig dat gedomineerd wordt door numerieke berekeningen. Voor dit artikel heb ik gekozen voor matrixvermenigvuldiging, en ik bereken de tijd die nodig is om twee 1000-bij-1000 matrices te vermenigvuldigen. I codeerde matrixvermenigvuldiging voor double op een eenvoudige manier, zoals weergegeven in lijst 2 hieronder. Hoewel er snellere manieren kunnen zijn om matrix vermenigvuldiging te implementeren( misschien met behulp van concurrency), is dat punt niet echt relevant voor dit artikel. Alles wat ik nodig heb is gemeenschappelijke code in twee soortgelijke methoden, een met behulp van de primitieve double en een met behulp van de wrapper klasse Double. De code voor het vermenigvuldigen van twee matrices van type Double is precies zoals die in lijst 2 met de voor de hand liggende typewijzigingen.

Lijst 2. Twee matrices van het type double

ik heb de twee methoden meerdere malen op mijn computer uitgevoerd om twee matrices van 1000 bij 1000 te vermenigvuldigen en de resultaten gemeten. De gemiddelde tijden zijn weergegeven in Tabel 2. In dit geval is de runtime-prestatie van double meer dan vier keer zo snel als die van Double. Dat is gewoon te veel verschil om te negeren.

de SciMark 2.0 benchmark

tot nu toe heb ik de enkelvoudige, eenvoudige benchmark van matrixvermenigvuldiging gebruikt om aan te tonen dat primitieven Aanzienlijk betere rekenprestaties kunnen opleveren dan objecten. Om mijn claims te versterken zal ik een meer wetenschappelijke benchmark gebruiken. SciMark 2.0 is een Java benchmark voor wetenschappelijke en numerieke computing beschikbaar van het National Institute of Standards and Technology (NIST). Ik heb de broncode voor deze benchmark gedownload en twee versies gemaakt, de originele versie met Primitieven en een tweede versie met wrapper classes. Voor de tweede versie heb ik int vervangen door Integer en double door Double om het volledige effect van het gebruik van wrapper classes te krijgen. Beide versies zijn beschikbaar in de broncode voor dit artikel.

download

John I. Moore, Jr.

de SciMark-benchmark meet de prestaties van verschillende computationele routines en rapporteert een samengestelde score in ongeveer Mflops (miljoenen floating point operaties per seconde). Dus, grotere aantallen zijn beter voor deze benchmark. Tabel 3 geeft de gemiddelde samengestelde scores van verschillende runs van elke versie van deze benchmark op mijn computer. Zoals getoond, de runtime uitvoeringen van de twee versies van de SciMark 2.0 benchmark waren consistent met de matrix vermenigvuldiging resultaten hierboven in dat de versie met primitieven was bijna vijf keer sneller dan de versie met behulp van wrapper klassen.

u hebt een paar variaties van Java-programma ‘ s gezien die numerieke berekeningen deden, waarbij zowel een benchmark van eigen bodem als een meer wetenschappelijke benchmark werd gebruikt. Maar hoe verhoudt Java zich tot andere talen? Ik sluit af met een snelle blik op hoe Java ‘ s prestaties zich verhoudt tot die van drie andere programmeertalen: Scala, C++, en JavaScript.

Benchmarking Scala

Scala is een programmeertaal die draait op de JVM en aan populariteit lijkt te winnen. Scala heeft een uniform type systeem, wat betekent dat het geen onderscheid maakt tussen Primitieven en objecten. Volgens Erik Osheim in Scala ‘ s numerieke type klasse (Pt. 1), Scala gebruikt primitieve types indien mogelijk, maar zal objecten gebruiken indien nodig. Ook Martin Odersky ’s beschrijving van Scala ‘ s Arrays zegt dat”… een Scala-array Array wordt weergegeven als een Java int, een Array wordt weergegeven als een Java double …”

dus betekent dit dat Scala ’s unified type systeem runtime prestaties vergelijkbaar met Java’ s primitieve types zal hebben? Eens kijken.

Ik ben niet zo bedreven met Scala als ik met Java ben, maar ik heb geprobeerd de code voor de matrix vermenigvuldigingsbenchmark rechtstreeks van Java naar Scala te converteren. Het resultaat wordt weergegeven in Lijst 3 hieronder. Toen ik de Scala-versie van de benchmark op mijn computer uitvoerde, was het gemiddeld 12.30 seconden, wat de prestaties van Scala erg dicht bij die van Java met primitieven brengt. Dat resultaat is veel beter dan ik had verwacht en ondersteunt de beweringen over hoe Scala omgaat met numerieke types.

download

John I. Moore, Jr.

Listing 3. Het vermenigvuldigen van twee matrices in Scala

Benchmarking C++

aangezien C++ direct op “bare metal” draait in plaats van op een virtuele machine, zou men natuurlijk verwachten dat C++ sneller zou draaien dan Java. Bovendien, Java prestaties wordt enigszins verminderd door het feit dat Java controleert toegang tot arrays om ervoor te zorgen dat elke index is binnen de aangegeven grenzen voor de array, terwijl C++ niet (een c++ functie die kan leiden tot buffer overflows, die kan worden benut door hackers). Ik vond C++ iets onhandig dan Java in het omgaan met fundamentele tweedimensionale arrays, maar gelukkig veel van deze onhandigheid kan worden verborgen in de prive-delen van een klasse. Voor C++ heb ik een eenvoudige versie van een Matrix klasse gemaakt, en ik heb de operator * overbelast voor het vermenigvuldigen van twee matrices, maar het basismatrixvermenigvuldigingsalgoritme werd direct geconverteerd vanuit de Java-versie. De C++ broncode wordt weergegeven in lijst 4.

download

John I. Moore, Jr.

lijst 4. Door twee matrices in C++

te vermenigvuldigen met Eclipse CDT (Eclipse for C++ Developers) met de MinGW C++ compiler, is het mogelijk om zowel debug-als release-versies van een toepassing te maken. Om C++ te testen heb ik de release-versie meerdere keren uitgevoerd en het gemiddelde van de resultaten. Zoals verwacht, C++ liep merkbaar sneller op deze eenvoudige benchmark, gemiddeld 7,58 seconden op mijn computer. Als ruwe prestaties de primaire factor is voor het selecteren van een programmeertaal, dan is C++ de taal van keuze voor numeriek-intensieve toepassingen.

Benchmarking JavaScript

OK, deze verraste me. Gezien het feit dat JavaScript is een zeer dynamische taal, ik verwachtte dat de prestaties van de slechtste van allemaal, nog erger dan Java met wrapper klassen. Maar in feite, JavaScript ‘ s prestaties was veel dichter bij die van Java met primitieven. Om JavaScript te testen heb ik Node geïnstalleerd.js, een JavaScript-engine met de reputatie zeer efficiënt te zijn. De resultaten waren gemiddeld 15,91 seconden. Vermelding 5 toont de JavaScript-versie van de matrix vermenigvuldiging benchmark die ik liep op Node.js

download

John I. Moore, Jr.

Listing 5. Het vermenigvuldigen van twee matrices in JavaScript

concluderend

toen Java 18 jaar geleden voor het eerst op het toneel verscheen, was het niet de beste taal vanuit prestatieperspectief voor toepassingen die gedomineerd worden door numerieke berekeningen. Maar na verloop van tijd, met technologische vooruitgang op gebieden zoals just-In-time (JIT) compilatie (aka adaptieve of dynamische compilatie), Java ‘ s prestaties voor dit soort toepassingen is nu vergelijkbaar met die van talen die worden gecompileerd in native code wanneer primitieve types worden gebruikt.

bovendien elimineren primitieven de behoefte aan afvalinzameling, waardoor primitieven een ander prestatievoordeel hebben ten opzichte van objecttypes. Tabel 4 geeft een overzicht van de runtime prestaties van de matrix vermenigvuldiging benchmark op mijn computer. Andere factoren zoals onderhoudbaarheid, draagbaarheid en expertise van ontwikkelaars maken Java een betere keuze voor veel van dergelijke toepassingen.

zoals eerder besproken, lijkt Oracle serieus na te denken over het verwijderen van Primitieven in een toekomstige versie van Java. Tenzij de Java compiler code kan genereren met prestaties vergelijkbaar met die van primitieven, ik denk dat hun verwijdering uit Java zou uitsluiten van het gebruik van Java voor bepaalde klassen van toepassingen; namelijk, die toepassingen gedomineerd door numerieke berekeningen. In dit artikel heb ik gebruik gemaakt van een eenvoudige benchmark op basis van matrix vermenigvuldiging en een meer wetenschappelijke benchmark, SciMark 2.0, om dit punt te argumenteren.

over de auteur

John I. Moore, Jr. Hoogleraar wiskunde en informatica aan de Citadel heeft een ruime ervaring in zowel de industrie als de academische wereld, met specifieke expertise op het gebied van objectgeoriënteerde technologie, software engineering en toegepaste wiskunde. Al meer dan drie decennia heeft Hij software ontworpen en ontwikkeld met behulp van relationele databases en verschillende high-order talen, en hij heeft uitgebreid gewerkt in Java sinds versie 1.1. Daarnaast heeft hij talrijke academische cursussen en industriële seminars over geavanceerde onderwerpen in de informatica ontwikkeld en onderwezen.

verder lezen

  1. Paul Krill schreef over Oracle ‘ s long-range plannen voor Java in “Oracle lays out long-range Java intentions” (JavaWorld, maart 2012). Dit artikel, samen met de bijbehorende commentaren draad, motiveerde me om deze verdediging van primitieven te schrijven.
  2. Szymon Guz schrijft over zijn resultaten in het benchmarken van primitieve types en wrapper classes in “Primitieven and objects benchmark in Java” (SimonOnSoftware, januari 2011).
  3. op de ondersteuningswebsite voor Programmeerprincipes en praktijk met behulp van C++ (Addison-Wesley, 2009) biedt C++ – Maker Bjarne Stroustrup een implementatie voor een matrixklasse die veel completer is dan die bij dit artikel.
  4. John Rose, Brian Goetz en Guy Steele bespreken een concept genaamd value types in “State of the Values” (OpenJDK.net, April 2014). Waarde types kunnen worden beschouwd als onveranderlijke door de gebruiker gedefinieerde geaggregeerde types zonder identiteit, therdoor het combineren van eigenschappen van zowel objecten en primitieven. De mantra voor waarde types is ” codes als een klasse, werkt als een int.”

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.