Un caz pentru păstrarea primitivelor în Java

primitivele au făcut parte din limbajul de programare Java de la lansarea sa inițială în 1996 și totuși rămân una dintre caracteristicile limbajului mai controversate. John Moore pledează puternic pentru păstrarea primitivelor în limbajul Java prin compararea reperelor Java simple, atât cu, cât și fără primitive. Apoi compară performanța Java cu cea a Scala, C++ și JavaScript într-un anumit tip de aplicație, unde primitivele fac o diferență notabilă.

întrebare: Care sunt cei mai importanți trei factori în achiziționarea de bunuri imobiliare?
răspuns: locație, locație, locație.

această zicală veche și adesea folosită este menită să sugereze că locația domină complet toți ceilalți factori atunci când vine vorba de imobiliare. Într-un argument similar, cei mai importanți trei factori de luat în considerare pentru utilizarea tipurilor primitive în Java sunt performanță, performanță, performanță. Există două diferențe între argumentul pentru imobiliare și argumentul pentru primitive. În primul rând, cu imobiliare, locația domină în aproape toate situațiile, dar câștigurile de performanță din utilizarea tipurilor primitive pot varia foarte mult de la un tip de aplicație la alta. În al doilea rând, cu imobiliare, există alți factori să ia în considerare, chiar dacă acestea sunt de obicei minore în comparație cu locația. Cu tipurile primitive, există un singur motiv pentru a le folosi — performanța; și apoi numai dacă aplicația este tipul care poate beneficia de utilizarea lor.

primitivele oferă o valoare redusă majorității aplicațiilor legate de afaceri și Internet care utilizează un model de programare client-server cu o bază de date pe backend. Dar performanța aplicațiilor dominate de calcule numerice poate beneficia foarte mult de utilizarea primitivelor.

includerea primitivelor în Java a fost una dintre cele mai controversate decizii de proiectare a limbajului, dovadă fiind numărul de articole și postări pe forum legate de această decizie. Simon Ritter a remarcat în discursul său principal Jax London din noiembrie 2011 că s-a acordat o atenție serioasă eliminării primitivelor într-o versiune viitoare a Java (vezi diapozitivul 41). În acest articol voi introduce pe scurt primitive și sistemul de tip Dual Java. Folosind mostre de cod și repere simple, voi face cazul meu pentru ce sunt necesare primitive Java pentru anumite tipuri de aplicații. De asemenea, voi compara performanța Java cu cea a Scala, C++ și JavaScript.

primitive versus obiecte

după cum probabil știți deja dacă citiți acest articol, Java are un sistem de tip dual, denumit de obicei tipuri primitive și tipuri de obiecte, adesea prescurtat pur și simplu ca primitive și obiecte. Există opt tipuri primitive predefinite în Java, iar numele lor sunt cuvinte cheie rezervate. Exemplele utilizate în mod obișnuit includ int, double și boolean. În esență, toate celelalte tipuri din Java, inclusiv toate tipurile definite de utilizator, sunt tipuri de obiecte. (Eu spun „în esență”, deoarece tipurile de matrice sunt un pic de un hibrid, dar ele sunt mult mai mult ca tipuri de obiecte decât tipuri primitive.) Pentru fiecare tip primitiv există o clasă de înveliș corespunzătoare care este un tip de obiect; exemplele includ Integer pentru int, Double pentru double și Boolean pentru boolean.

tipurile Primitive sunt bazate pe valoare, dar tipurile de obiecte sunt bazate pe referință și aici se află atât puterea, cât și sursa controversei tipurilor primitive. Pentru a ilustra diferența, luați în considerare cele două declarații de mai jos. Prima declarație folosește un tip primitiv, iar a doua folosește o clasă de înveliș.

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

folosind autoboxing, o caracteristică adăugată la JDK 5, aș putea scurta a doua declarație la pur și simplu

Integer n2 = 100;

dar semantica de bază nu se schimbă. Autoboxingul simplifică utilizarea claselor de înveliș și reduce cantitatea de cod pe care un programator trebuie să o scrie, dar nu schimbă nimic în timpul rulării.

diferența dintre primitivul n1 și obiectul învelișului n2 este ilustrată de diagrama din Figura 1.

John I. Moore, Jr.

Figura 1. Dispunerea memoriei primitivelor versus obiecte

variabila n1 deține o valoare întreagă, dar variabila n2 conține o referință la un obiect și este obiectul care deține valoarea întreagă. În plus, obiectul la care se face referire prin n2 conține, de asemenea, o referință la obiectul clasei Double.

problema primitivelor

înainte de a încerca să vă conving de nevoia de tipuri primitive, ar trebui să recunosc că mulți oameni nu vor fi de acord cu mine. Sherman Alpert în” tipuri Primitive considerate dăunătoare „susține că primitivele sunt dăunătoare deoarece amestecă” semantica procedurală într-un model altfel uniform orientat pe obiecte. Primitivele nu sunt obiecte de primă clasă, dar există într-un limbaj care implică, în primul rând, obiecte de primă clasă.”Primitivele și obiectele (sub formă de clase de înveliș) oferă două modalități de manipulare logică a tipurilor similare, dar au semantică de bază foarte diferită. De exemplu, cum ar trebui comparate două instanțe pentru egalitate? Pentru tipurile primitive, se folosește operatorul ==, dar pentru obiecte alegerea preferată este să apelați metoda equals(), care nu este o opțiune pentru primitive. În mod similar, există diferite semantici atunci când se atribuie valori sau se transmit parametri. Chiar și valorile implicite sunt diferite; de exemplu, 0 pentru int versus null pentru Integer.

pentru mai multe informații despre această problemă, consultați postarea de pe blogul lui Eric Bruno, „o discuție primitivă modernă”, care rezumă unele dintre avantajele și dezavantajele primitivelor. O serie de discuții despre depășirea stivei se concentrează, de asemenea, pe primitive, inclusiv „de ce oamenii folosesc în continuare tipuri primitive în Java?”și” există un motiv pentru a folosi întotdeauna obiecte în loc de primitive?.”Programatorii Stack Exchange găzduiește o discuție similară intitulată” când să folosiți clasa primitivă vs în Java?”.

utilizarea memoriei

a double în Java ocupă întotdeauna 64 de biți în memorie, dar dimensiunea unei referințe depinde de mașina virtuală Java (JVM). Computerul meu rulează versiunea pe 64 de biți a Windows 7 și un JVM pe 64 de biți și, prin urmare, o referință pe computerul meu ocupă 64 de biți. Pe baza diagramei din Figura 1, m — aș aștepta ca un singur double cum ar fi n1 să ocupe 8 octeți (64 biți) și m-aș aștepta ca un singur Double cum ar fi n2 să ocupe 24 octeți-8 pentru referința la obiect, 8 pentru valoarea double stocată în obiect și 8 pentru referința la obiectul de clasă pentru Double. În plus, Java folosește memorie suplimentară pentru a sprijini colectarea gunoiului pentru tipurile de obiecte, dar nu și pentru tipurile primitive. Să verificăm.

folosind o abordare similară cu cea a lui Glen McCluskey în ” Java primitive types vs. „metoda prezentată în lista 1 măsoară numărul de octeți ocupați de o matrice n-cu-n (matrice bidimensională) de double.

listă 1. Calculând utilizarea memoriei de tip double

modificând codul din lista 1 cu modificările de tip evidente (nu sunt afișate), putem măsura și numărul de octeți ocupați de o matrice n-Cu-N de Double. Când testez aceste două metode pe computerul meu folosind matrice 1000-by-1000, obțin rezultatele prezentate în tabelul 1 de mai jos. După cum este ilustrat, versiunea pentru tipul primitiv double echivalează cu puțin mai mult de 8 octeți pe intrare în matrice, aproximativ ceea ce mă așteptam. Cu toate acestea, versiunea pentru tipul de obiect Double a necesitat puțin mai mult de 28 de octeți pe intrare în matrice. Astfel, în acest caz, utilizarea memoriei Double este de peste trei ori mai mare decât utilizarea memoriei double, ceea ce nu ar trebui să fie o surpriză pentru oricine înțelege aspectul memoriei ilustrat în Figura 1 de mai sus.

runtime performance

pentru a compara performanțele runtime pentru primitive și obiecte, avem nevoie de un algoritm dominat de calcule numerice. Pentru acest articol am ales multiplicarea matricei și calculez timpul necesar pentru a înmulți două matrice 1000-cu-1000. Am codificat multiplicarea matricei pentru double într-o manieră simplă, așa cum se arată în Lista 2 de mai jos. Deși pot exista modalități mai rapide de a implementa multiplicarea matricei (poate folosind concurența), acest punct nu este cu adevărat relevant pentru acest articol. Tot ce am nevoie este codul comun în două metode similare, una folosind primitivul double și una folosind clasa wrapper Double. Codul pentru înmulțirea a două matrice de tip Double este exact așa în listarea 2 cu modificările de tip evidente.

Listă 2. Înmulțind două matrice de tip dublu

am rulat cele două metode pentru a multiplica două matrice 1000-cu-1000 pe computerul meu de mai multe ori și am măsurat rezultatele. Timpii medii sunt prezentați în tabelul 2. Astfel, în acest caz, performanța de rulare a double este de peste patru ori mai rapidă decât cea a Double. Aceasta este pur și simplu o diferență prea mare de ignorat.

benchmark-ul SciMark 2.0

până acum am folosit benchmark-ul simplu și simplu al multiplicării matricei pentru a demonstra că primitivele pot produce performanțe de calcul semnificativ mai mari decât obiectele. Pentru a-mi consolida afirmațiile, voi folosi un punct de referință mai științific. SciMark 2.0 Este un punct de referință Java pentru calculul științific și numeric disponibil de la Institutul Național de standarde și Tehnologie (NIST). Am descărcat codul sursă pentru acest benchmark și am creat două versiuni, versiunea originală folosind primitive și o a doua versiune folosind clase de înveliș. Pentru a doua versiune am înlocuit int cu Integer și double cu Double pentru a obține efectul complet al utilizării claselor de înveliș. Ambele versiuni sunt disponibile în codul sursă pentru acest articol.

download

John I. Moore, Jr.

SciMark benchmark măsoară performanța mai multor rutine de calcul și raportează un scor compozit în mflops aproximative (milioane de operațiuni în virgulă mobilă pe secundă). Astfel, un număr mai mare este mai bun pentru acest punct de referință. Tabelul 3 oferă scorurile compozite medii din mai multe rulări ale fiecărei versiuni a acestui punct de referință pe computerul meu. După cum s-a arătat, performanțele de rulare ale celor două versiuni ale benchmark-ului SciMark 2.0 au fost în concordanță cu rezultatele multiplicării matricei de mai sus, în sensul că versiunea cu primitive a fost de aproape cinci ori mai rapidă decât versiunea folosind clase de înveliș.

ați văzut câteva variante de programe Java care fac calcule numerice, folosind atât un benchmark de origine, cât și unul mai științific. Dar cum se compară Java cu alte limbi? Voi încheia cu o privire rapidă asupra modului în care performanța Java se compară cu cea a altor trei limbaje de programare: Scala, C++ și JavaScript.

Benchmarking Scala

Scala este un limbaj de programare care rulează pe JVM și pare să câștige popularitate. Scala are un sistem de tip unificat, ceea ce înseamnă că nu face distincție între primitive și obiecte. Potrivit lui Erik Osheim în clasa de tip Numeric Scala (pt. 1), Scala folosește tipuri primitive atunci când este posibil, dar va folosi obiecte dacă este necesar. În mod similar, descrierea lui Martin Odersky a matricelor Scala spune că „… un tablou Scala Array este reprezentat ca Java int, un Array este reprezentat ca Java double …”

deci, asta înseamnă că sistemul de tip unificat Scala va avea performanțe de rulare comparabile cu tipurile primitive Java? Să vedem.

nu sunt la fel de priceput cu Scala ca și cu Java, dar am încercat să convertesc codul pentru benchmark-ul de multiplicare matrix direct de la Java la Scala. Rezultatul este prezentat în Lista 3 de mai jos. Când am rulat versiunea Scala a benchmark-ului pe computerul meu, aceasta a avut o medie de 12,30 secunde, ceea ce pune performanța Scala foarte aproape de cea a Java cu primitive. Acest rezultat este mult mai bun decât mă așteptam și susține afirmațiile despre modul în care Scala gestionează tipurile numerice.

descarca

John I. Moore, Jr.

listare 3. Înmulțirea a două matrice în Scala

Benchmarking C++

deoarece c++ rulează direct pe „metal gol”, mai degrabă decât într-o mașină virtuală, s-ar aștepta în mod natural C++ pentru a rula mai repede decât Java. Mai mult, performanța Java este redusă ușor de faptul că Java verifică accesul la matrice pentru a se asigura că fiecare index se află în limitele declarate pentru matrice, în timp ce C++ nu (o caracteristică C++ care poate duce la revărsări de tampon, care pot fi exploatate de hackeri). Am găsit C++ ca fiind ceva mai ciudat decât Java în tratarea matricelor bidimensionale de bază, dar, din fericire, o mare parte din această stângăcie poate fi ascunsă în părțile private ale unei clase. Pentru C++, am creat o versiune simplă a unei clase Matrix și am supraîncărcat operatorul * pentru înmulțirea a două matrice, dar algoritmul de multiplicare a matricei de bază a fost convertit direct din versiunea Java. Codul sursă C++ este prezentat în Lista 4.

descarca

John I. Moore, Jr.

listare 4. Înmulțind două matrice în C++

folosind Eclipse CDT (Eclipse for C++ Developers) cu compilatorul MinGW C++, este posibil să creați atât versiuni de depanare, cât și versiuni de lansare ale unei aplicații. Pentru a testa c++ am rulat versiunea de lansare de mai multe ori și am mediat rezultatele. Așa cum era de așteptat, c++ a rulat vizibil mai repede pe acest punct de referință simplu, în medie 7,58 secunde pe computerul meu. Dacă performanța brută este factorul principal pentru selectarea unui limbaj de programare, atunci C++ este limbajul de alegere pentru aplicațiile intensive numeric.

Benchmarking JavaScript

bine, asta ma surprins. Având în vedere că JavaScript este un limbaj foarte dinamic, m-am așteptat ca performanța sa să fie cea mai rea dintre toate, chiar mai rea decât Java cu clase de înveliș. Dar, de fapt, performanța JavaScript a fost mult mai apropiată de cea a Java cu primitive. Pentru a testa JavaScript am instalat nodul.js, un motor JavaScript cu reputația de a fi foarte eficient. Rezultatele au fost în medie de 15,91 secunde. Listarea 5 arată versiunea JavaScript a benchmark-ului matrix multiplication pe care l-am rulat pe Node.js

descărcare

John I. Moore, Jr.

listare 5. Înmulțirea a două matrice în JavaScript

în concluzie

când Java a ajuns pentru prima dată pe scenă în urmă cu aproximativ 18 ani, nu a fost cel mai bun limbaj din perspectiva performanței pentru aplicațiile dominate de calcule numerice. Dar, în timp, cu progresele tehnologice în domenii precum compilarea just-in-time (JIT) (aka compilație adaptivă sau dinamică), performanța Java pentru aceste tipuri de aplicații este acum comparabilă cu cea a limbajelor care sunt compilate în cod nativ atunci când sunt utilizate tipuri primitive.

mai mult, primitivele elimină nevoia de colectare a gunoiului, oferind astfel un alt avantaj de performanță al primitivelor față de tipurile de obiecte. Tabelul 4 rezumă performanța runtime a benchmark-ului matrix multiplication de pe computerul meu. Alți factori, cum ar fi mentenabilitatea, portabilitatea și expertiza dezvoltatorilor, fac din Java o alegere mai bună pentru multe astfel de aplicații.

după cum sa discutat anterior, Oracle pare să acorde o atenție serioasă eliminării primitivelor într-o versiune viitoare a Java. Cu excepția cazului în care compilatorul Java poate genera cod cu performanțe comparabile cu cele ale primitivelor, cred că eliminarea lor din Java ar exclude utilizarea Java pentru anumite clase de aplicații; și anume, acele aplicații dominate de calcule numerice. În acest articol am folosit un punct de referință simplu bazat pe multiplicarea matricei și un punct de referință mai științific, SciMark 2.0, Pentru a argumenta acest punct.

despre autor

John I. Moore, Jr., Profesor de Matematică și Informatică la Citadel, are o gamă largă de experiență atât în industrie, cât și în mediul academic, cu expertiză specifică în domeniile tehnologiei orientate pe obiecte, ingineriei software și matematicii aplicate. Timp de mai bine de trei decenii a proiectat și dezvoltat software folosind baze de date relaționale și mai multe limbi de ordin înalt și a lucrat intens în Java de la versiunea 1.1. În plus, a dezvoltat și a predat numeroase cursuri academice și seminarii industriale pe teme avansate în informatică.

lecturi suplimentare

  1. Paul Krill a scris despre planurile Oracle pe termen lung pentru Java în „Oracle stabilește intențiile Java pe termen lung” (JavaWorld, martie 2012). Acest articol, împreună cu firul de comentarii asociate, m-au motivat să scriu această apărare a primitivelor.
  2. Szymon Guz scrie despre rezultatele sale în benchmarking tipuri primitive și clase de înveliș în „Primitives and objects benchmark in Java” (SimonOnSoftware, ianuarie 2011).
  3. pe site — ul de asistență pentru programare-principii și practică folosind C++ (Addison-Wesley, 2009), creatorul c++ Bjarne Stroustrup oferă o implementare pentru o clasă de matrice mult mai completă decât cea care însoțește acest articol.
  4. John Rose, Brian Goetz și Guy Steele discută un concept numit tipuri de valori în „starea valorilor” (OpenJDK.net, aprilie 2014). Tipurile de valori pot fi considerate tipuri agregate imuabile definite de utilizator fără identitate, prin combinarea proprietăților atât ale obiectelor, cât și ale primitivelor. Mantra pentru tipurile de valoare este ” Coduri ca o clasă, funcționează ca un int.”

Lasă un răspuns

Adresa ta de email nu va fi publicată.