Ohjelmointi

Tapaus primitiivien pitämiseen Java-tilassa

Alkukielet ovat olleet osa Java-ohjelmointikieliä sen alkuperäisestä julkaisusta vuonna 1996, ja silti ne ovat edelleen yksi kiistanalaisimmista kieliominaisuuksista. John Moore on vahva esimerkki primitiivien pitämisestä Java-kielellä vertaamalla yksinkertaisia ​​Java-vertailuarvoja sekä primitiivien kanssa että ilman. Sitten hän vertaa Java-suorituskykyä Scalan, C ++: n ja JavaScriptin suorituskykyyn tietyntyyppisissä sovelluksissa, joissa primitiivit tekevät merkittävän eron.

Kysymys: Mitkä ovat kolme tärkeintä tekijää kiinteistöjen ostamisessa?

Vastaus: Sijainti, sijainti, sijainti.

Tämän vanhan ja usein käytetyn sanonnan on tarkoitus tarkoittaa, että sijainti hallitsee täysin kaikkia muita tekijöitä kiinteistöjen suhteen. Samankaltaisessa argumentissa kolme tärkeintä tekijää, jotka on otettava huomioon primitiivisten tyyppien käytössä Javassa, ovat suorituskyky, suorituskyky ja suorituskyky. Kiinteistöjä koskevan argumentin ja primitiivien argumentin välillä on kaksi eroa. Ensinnäkin kiinteistöjen kohdalla sijainti dominoi melkein kaikissa tilanteissa, mutta primitiivisten tyyppien käytöstä saatavat suorituskyvyn hyödyt voivat vaihdella suuresti sovelluksista toiseen. Toiseksi kiinteistöjen suhteen on otettava huomioon muita tekijöitä, vaikka ne ovat yleensä pieniä sijaintiin verrattuna. Alkeellisilla tyypeillä on vain yksi syy käyttää niitä - esitys; ja sitten vain, jos sovellus on sellainen, joka voi hyötyä niiden käytöstä.

Primitiivit tarjoavat vähän arvoa useimmille yritystoimintaan liittyville ja Internet-sovelluksille, jotka käyttävät asiakas-palvelin-ohjelmointimallia ja tietokanta taustalla. Mutta numeeristen laskelmien hallitseman sovelluksen suorituskyky voi hyötyä suuresti primitiivien käytöstä.

Primitiivien sisällyttäminen Java-ohjelmaan on ollut yksi kiistanalaisimmista kielisuunnittelupäätöksistä, minkä osoittaa tähän päätökseen liittyvien artikkeleiden ja foorumiviestien määrä. Simon Ritter huomautti JAX Londonissa marraskuussa 2011 pitämässään puheessa, että primitiivien poistamista harkittiin vakavasti Java-version tulevassa versiossa (katso dia 41). Tässä artikkelissa esitän lyhyesti primitiivit ja Java-kaksoistyyppisen järjestelmän. Koodinäytteiden ja yksinkertaisten vertailuarvojen avulla esitän tapaukseni siitä, miksi Java-primitiivejä tarvitaan tietyntyyppisiin sovelluksiin. Vertailen myös Java-suorituskykyä Scalan, C ++: n ja JavaScriptin suorituskykyyn.

Ohjelmiston suorituskyvyn mittaaminen

Ohjelmiston suorituskyky mitataan yleensä ajan ja tilan perusteella. Aika voi olla todellinen ajoaika, kuten 3,7 minuuttia, tai kasvun järjestys syötteen koon perusteella, kuten O(n2). Vastaavia mittaustiloja on tilan suorituskyvylle, joka ilmaistaan ​​usein päämuistin käyttönä, mutta voi ulottua myös levyn käyttöön. Suorituskyvyn parantamiseen liittyy yleensä aika-avaruus-kompromissi, sillä ajan parantamiseksi tehdyillä muutoksilla on usein haitallinen vaikutus avaruuteen ja päinvastoin. Kasvun järjestysmittaus riippuu algoritmista, eikä vaihtaminen kääreluokista primitiiveiksi muuta tulosta. Mutta kun on kyse todellisesta aika- ja tilatehokkuudesta, primitiivien käyttö kääreluokkien sijasta tarjoaa parannuksia sekä ajassa että tilassa samanaikaisesti.

Primitiivit vs. esineet

Kuten luultavasti jo tiedät lukiessasi tätä artikkelia, Java: lla on kaksoistyyppinen järjestelmä, jota yleensä kutsutaan primitiivisiksi tyypeiksi ja objektityypeiksi ja jotka usein lyhennetään yksinkertaisesti primitiiveiksi ja esineiksi. Javalassa on ennalta määritelty kahdeksan primitiivistä tyyppiä, ja niiden nimet ovat varattuja avainsanoja. Yleisesti käytettyjä esimerkkejä ovat int, kaksinkertainenja looginen. Pohjimmiltaan kaikki muut Java-tyypit, mukaan lukien kaikki käyttäjän määrittämät tyypit, ovat objektityyppejä. (Sanon "olennaisesti", koska matriisityypit ovat vähän hybridiä, mutta ne ovat paljon enemmän kuin objektityypit kuin primitiiviset tyypit.) Jokaiselle primitiiviselle tyypille on vastaava käärintäluokka, joka on objektityyppi; esimerkkejä ovat Kokonaisluku varten int, Kaksinkertainen varten kaksinkertainenja Boolen varten looginen.

Primitiiviset tyypit ovat arvopohjaisia, mutta objektityypit ovat referenssipohjaisia, ja siinä on sekä primitiivisten tyyppien voima että kiistan lähde. Harkitse eroa tarkastelemalla kahta alla olevaa ilmoitusta. Ensimmäisessä ilmoituksessa käytetään primitiivistä tyyppiä ja toisessa kääre-luokkaa.

 int n1 = 100; Kokonaisluku n2 = uusi kokonaisluku (100); 

JDK 5: ään lisätty ominaisuus, autoboxing, voisin lyhentää toista ilmoitusta yksinkertaisesti

 Kokonaisluku n2 = 100; 

mutta taustalla oleva semantiikka ei muutu. Autoboxing yksinkertaistaa kääreen luokkien käyttöä ja vähentää ohjelmoijan kirjoittaman koodin määrää, mutta se ei muuta mitään ajon aikana.

Ero primitiivisen välillä n1 ja kääreobjekti n2 kuvaa kaavio kuvassa 1.

John I.Moore, nuorempi

Muuttuja n1 sisältää kokonaisluvun arvon, mutta muuttuja n2 sisältää viittauksen objektiin, ja se on objekti, jolla on kokonaisluku. Lisäksi objekti, johon viitataan n2 sisältää myös viittauksen luokan objektiin Kaksinkertainen.

Primitiivien ongelma

Ennen kuin yritän vakuuttaa sinut primitiivisten tyyppien tarpeesta, minun on tunnustettava, että monet ihmiset eivät ole samaa mieltä kanssani. Sherman Alpert julkaisussa "Primitiivityypit, joita pidetään haitallisina" väittää, että primitiivit ovat haitallisia, koska ne sekoittavat "menettelysemantiikan muuten yhtenäiseksi olio-malliksi. Primitiivit eivät ole ensiluokkaisia ​​esineitä, mutta ne ovat olemassa kielellä, joka sisältää ensisijaisesti luokan esineitä. " Primitiivit ja esineet (kääreluokkien muodossa) tarjoavat kaksi tapaa käsitellä loogisesti samanlaisia ​​tyyppejä, mutta niiden taustalla oleva semantiikka on hyvin erilainen. Esimerkiksi kuinka kahta tapausta tulisi verrata tasa-arvoon? Alkeistyypeille käytetään == operaattori, mutta objektien kannalta ensisijainen valinta on soittaa on yhtä suuri () menetelmä, joka ei ole vaihtoehto primitiiveille. Vastaavasti eri semantiikkaa on olemassa, kun määritetään arvoja tai välitetään parametreja. Jopa oletusarvot ovat erilaiset; esimerkiksi., 0 varten int vastaan tyhjä varten Kokonaisluku.

Lisää taustatietoa aiheesta löytyy Eric Brunon blogikirjoituksesta "Moderni primitiivinen keskustelu", jossa esitetään yhteenveto primitiivien eduista ja haitoista. Monet pinon ylivuotoa koskevissa keskusteluissa keskittyvät myös primitiiveihin, mukaan lukien "Miksi ihmiset käyttävät edelleen primitiivisiä tyyppejä Javassa?" ja "Onko syytä käyttää aina esineitä primitiivien sijaan?" Ohjelmoijat Stack Exchange isännöi samanlaista keskustelua "Milloin käyttää primitiivistä vs luokkaa Javassa?".

Muistin käyttö

A kaksinkertainen Java: ssa on aina 64 bittiä muistissa, mutta viitteen koko riippuu Java-virtuaalikoneesta (JVM). Tietokoneessani on Windows 7: n 64-bittinen versio ja 64-bittinen JVM, ja siksi tietokoneessani oleva viite vie 64 bittiä. Kuvion 1 kaavion perusteella odotan yhden kaksinkertainen kuten n1 käyttää 8 tavua (64 bittiä), ja odotan yhden Kaksinkertainen kuten n2 käyttää 24 tavua - 8 viittausta esineeseen, 8 merkkiä kaksinkertainen objektiin tallennettu arvo ja 8 viitteeksi luokan objektille Kaksinkertainen. Lisäksi Java käyttää ylimääräistä muistia roskakorin tukemiseen objektityypeille, mutta ei primitiivisille tyypeille. Katsotaanpa se.

Käyttämällä lähestymistapaa, joka on samanlainen kuin Glen McCluskeyssä "Java-primitiiviset tyypit vs. kääreet" -menetelmässä, luettelossa 1 esitetty menetelmä mittaa tavujen lukumäärän, jonka n-by-n-matriisi (kaksiulotteinen taulukko) on varattu. kaksinkertainen.

Luettelo 1. Lasketaan tyypin double muistin käyttöaste

 julkinen staattinen pitkä getBytesUsingPrimitives (int n) {System.gc (); // pakota roskien keräys pitkä memStart = Runtime.getRuntime (). freeMemory (); kaksinkertainen [] [] a = uusi kaksinkertainen [n] [n]; // laita matriisiin joitain satunnaisia ​​arvoja (int i = 0; i <n; ++ i) {varten (int j = 0; j <n; ++ j) a [i] [j] = matematiikka. satunnainen (); } pitkä memEnd = Runtime.getRuntime (). freeMemory (); palaa memStart - memEnd; } 

Jos muokkaat luettelon 1 koodia ilmeisillä tyyppimuutoksilla (ei esitetty), voimme myös mitata tavujen lukumäärän, jonka n-by-n-matriisi käyttää Kaksinkertainen. Kun testaan ​​näitä kahta menetelmää tietokoneellani käyttämällä 1000 x 1000 matriiseja, saan alla olevassa taulukossa 1 esitetyt tulokset. Kuten on esitetty, primitiivisen tyypin versio kaksinkertainen vastaa hieman yli 8 tavua matriisin merkintää kohti, suunnilleen mitä odotin. Objektityypin versio Kaksinkertainen vaati hieman yli 28 tavua matriisia kohti. Siten tässä tapauksessa muistin käyttö Kaksinkertainen on yli kolme kertaa muistin käyttöaste kaksinkertainen, jonka ei pitäisi olla yllätys kenelle tahansa, joka ymmärtää yllä olevassa kuvassa 1 esitetyn muistin asettelun.

Taulukko 1. Tuplan vs. kaksoismuistin käyttö

VersioTavuja yhteensäTavua tavua kohden
Käyttämällä kaksinkertainen8,380,7688.381
Käyttämällä Kaksinkertainen28,166,07228.166

Suorituskyky

Primitiivien ja esineiden ajonaikaisen suorituskyvyn vertaamiseksi tarvitsemme algoritmin, jota hallitsevat numeeriset laskelmat. Tälle artikkelille olen valinnut matriisikertomuksen ja lasken kahden 1000-1000-matriisin kertomiseen tarvittavan ajan. Koodasin matriisikertoimen kaksinkertainen suoraviivaisella tavalla alla olevan luettelon 2 mukaisesti. Vaikka matriisikertomisen toteuttaminen voi olla nopeampaa (kenties samanaikaisuuden käyttäminen), tämä kohta ei ole oikeastaan ​​merkityksellinen tässä artikkelissa. Tarvitsen vain yhteisen koodin kahdessa samankaltaisessa menetelmässä, joista toisessa käytetään primitiivistä kaksinkertainen ja yksi kääreluokasta Kaksinkertainen. Koodi kahden tyyppisen matriisin kertomiseksi Kaksinkertainen on täsmälleen samanlainen kuin Listing 2: ssa, jossa ilmeiset tyyppimuutokset.

Listaus 2. Kertomalla kaksi matriisia tyypin double

 julkinen staattinen kaksinkertainen [] [] kertolasku (kaksinkertainen [] [] a, kaksinkertainen [] [] b) {if (! checkArgs (a, b)) heittää uuden IllegalArgumentExceptionin ("Matriisit eivät ole yhteensopivia kertolaskun kanssa"); int nRivit = a. pituus; int nCols = b [0] .pituus; double [] [] tulos = uusi double [nRows] [nCols]; for (int riviNum = 0; riviNum <nRivit; ++ riviNum) {for (int kolNum = 0; kolNum <nCols; ++ kolNum) {kaksinkertainen summa = 0,0; for (int i = 0; i <a [0] .pituus; ++ i) summa + = a [riviNum] [i] * b [i] [colNum]; tulos [riviNum] [kolNum] = summa; }} palautustulos; } 

Suoritin nämä kaksi tapaa kertoa tietokoneelleni kaksi 1000 x 1000 matriisia useita kertoja ja mitasin tulokset. Keskimääräiset ajat on esitetty taulukossa 2. Täten tässä tapauksessa kaksinkertainen on yli neljä kertaa nopeampi kuin Kaksinkertainen. Se on yksinkertaisesti liian suuri ero sivuutettavaksi.

Taulukko 2. Tuplan vs. tuplan ajonaikainen suorituskyky

VersioSekuntia
Käyttämällä kaksinkertainen11.31
Käyttämällä Kaksinkertainen48.48

SciMark 2.0 -vertailu

Toistaiseksi olen käyttänyt matriisikertomisen yhtä yksinkertaista vertailuarvoa osoittaakseni, että primitiivit voivat tuottaa huomattavasti suurempaa laskentatehoa kuin kohteet. Vahvistan väitteitäni käyttämällä tieteellisempää vertailuarvoa. SciMark 2.0 on tieteellisen ja numeerisen laskennan Java-vertailuarvo, jonka saa National Institute of Standards and Technology (NIST). Latasin tämän vertailuarvon lähdekoodin ja loin kaksi versiota, alkuperäisessä versiossa primitiivit ja toisen version kääreissä. Vaihdoin toisen version int kanssa Kokonaisluku ja kaksinkertainen kanssa Kaksinkertainen saadaksesi täyden hyödyn kääreluokkien käytöstä. Molemmat versiot ovat saatavilla tämän artikkelin lähdekoodissa.

lataa Benchmarking Java: Lataa lähdekoodi John I. Moore, Jr.

SciMark-vertailuarvo mittaa useiden laskennallisten rutiinien suorituskykyä ja raportoi yhdistetyn pistemäärän likimääräisinä Mflops-arvoina (miljoonia liukulukuoperaatioita sekunnissa). Siksi suuremmat luvut ovat parempia tälle vertailuarvolle. Taulukko 3 antaa keskimääräiset yhdistetyt pisteet useista tämän vertailuarvon versioista tietokoneellani. Kuten on esitetty, SciMark 2.0 -vertailuindeksin kahden version ajonaikaiset suorituskyvyt olivat yhdenmukaisia ​​yllä olevien matriisikertolaskujen kanssa, koska primitiiveillä varustettu versio oli melkein viisi kertaa nopeampi kuin kääreluokkia käyttävä versio.

Taulukko 3. SciMark-vertailuarvon ajonaikainen suorituskyky

SciMark-versioSuorituskyky (Mflops)
Primitiivien käyttö710.80
Kääreiden käyttäminen143.73

Olet nähnyt muutamia Java-ohjelmien muunnelmia, jotka tekevät numeerisia laskutoimituksia käyttäen sekä kotimaista että tieteellisempää vertailuarvoa. Mutta miten Java vertaa muihin kieliin? Lopuksi tarkastelen nopeasti, kuinka Java suorituskyky vertautuu kolmen muun ohjelmointikielen: Scala, C ++ ja JavaScript.

Scalan vertailu

Scala on JVM: llä toimiva ohjelmointikieli, joka näyttää olevan kasvava suosio. Scalalla on yhtenäinen tyyppijärjestelmä, eli se ei tee eroa primitiivien ja esineiden välillä. Erik Osheimin mukaan Scalan Numeric-tyyppiluokassa (pt. 1) Scala käyttää primitiivisiä tyyppejä mahdollisuuksien mukaan, mutta käyttää esineitä tarvittaessa. Samoin Martin Oderskyn kuvaus Scalan matriiseista sanoo, että "... Scala-taulukko Taulukko [Int] on edustettuina Java int [], an Taulukko [kaksinkertainen] on edustettuina Java kaksinkertainen[] ..."

Tarkoittaako tämä siis sitä, että Scalan yhtenäisellä tyyppijärjestelmällä on ajonaikainen suorituskyky, joka on verrattavissa Javan primitiivisiin tyyppeihin? Katsotaan.

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