primitivy jsou součástí programovacího jazyka Java od jeho prvního vydání v roce 1996, a přesto zůstávají jednou z kontroverznějších jazykových funkcí. John Moore je silným argumentem pro udržení primitiv v jazyce Java porovnáním jednoduchých benchmarků Java, a to jak s primitivy, tak bez nich. Poté porovnává výkon Javy s výkonem Scala, C++ a JavaScriptu v konkrétním typu aplikace, kde primitivy dělají pozoruhodný rozdíl.
otázka: Jaké jsou tři nejdůležitější faktory při nákupu nemovitostí?
odpověď: umístění, umístění, umístění.
toto staré a často používané pořekadlo má znamenat, že umístění zcela dominuje všem ostatním faktorům, pokud jde o nemovitosti. V podobném argumentu jsou tři nejdůležitější faktory, které je třeba zvážit při používání primitivních typů v Javě, výkon, výkon. Existují dva rozdíly mezi argumentem pro nemovitosti a argumentem pro primitivy. Za prvé, s nemovitostmi dominuje umístění téměř ve všech situacích, ale výkonnostní zisky z používání primitivních typů se mohou značně lišit od jednoho druhu aplikace k druhému. Za druhé, s nemovitostmi, existují další faktory, které je třeba zvážit, i když jsou obvykle menší ve srovnání s umístěním. U primitivních typů existuje pouze jeden důvod, proč je používat-výkon; a pak pouze v případě, že aplikace je taková, která může těžit z jejich použití.
Primitiva nabízejí malou hodnotu pro většinu obchodních a internetových aplikací, které používají programovací model klient-server s databází na backendu. Výkon aplikací, kterým dominují numerické výpočty, však může z použití primitiv velmi těžit.
zahrnutí primitiv do Javy bylo jedním z kontroverznějších rozhodnutí o designu jazyků, o čemž svědčí počet článků a příspěvků na fóru souvisejících s tímto rozhodnutím. Simon Ritter poznamenal ve svém hlavním projevu JAX London v listopadu 2011, že se vážně uvažuje o odstranění primitiv v budoucí verzi Java(viz snímek 41). V tomto článku stručně představím primitivy a duální systém Java. Pomocí ukázek kódu a jednoduchých benchmarků se budu zabývat tím, proč jsou Java primitivy potřebné pro určité typy aplikací. Budu také porovnávat výkon Java s výkonem Scala, C++ a JavaScript.
- primitivy versus objekty
- problém s primitivy
- využití paměti
- Výpis 1. Výpočet využití paměti typu double
- Runtime performance
- Výpis 2. Vynásobením dvou matic typu double
- scimark 2.0 benchmark
- Benchmarking Scala
- Výpis 3. Vynásobením dvou matic v Scala
- Benchmarking C++
- Výpis 4. Vynásobením dvou matic v C++
- Benchmarking JavaScript
- Výpis 5. Násobení dvou matic v JavaScriptu
- na závěr
- o autorovi
- další čtení
primitivy versus objekty
jak již pravděpodobně víte, pokud čtete tento článek, Java má systém dvojího typu, obvykle označovaný jako primitivní typy a typy objektů, často zkráceně jednoduše jako primitivy a objekty. V Javě je předdefinováno osm primitivních typů a jejich názvy jsou vyhrazena klíčová slova. Mezi běžně používané příklady patří int
, double
a boolean
. V podstatě všechny ostatní typy v Javě, včetně všech uživatelem definovaných typů, jsou typy objektů. (Říkám „v podstatě“ , protože typy polí jsou trochu hybrid, ale jsou mnohem více jako typy objektů než primitivní typy.)Pro každý primitivní typ existuje odpovídající třída obalů, která je typem objektu; příklady zahrnují Integer
pro int
, Double
pro double
a Boolean
pro boolean
.
primitivní typy jsou založeny na hodnotách, ale typy objektů jsou založeny na referencích, a v tom spočívá jak síla, tak zdroj kontroverze primitivních typů. Pro ilustraci rozdílu zvažte dvě níže uvedená prohlášení. První deklarace používá primitivní typ a druhá používá třídu obalů.
int n1 = 100;Integer n2 = new Integer(100);
pomocí autoboxingu, funkce přidané do JDK 5, bych mohl zkrátit druhou deklaraci na jednoduše
Integer n2 = 100;
, ale základní sémantika se nemění. Autoboxing zjednodušuje používání tříd wrapperů a snižuje množství kódu, který musí programátor napsat, ale za běhu nic nemění.
rozdíl mezi primitivním n1
a objektem obalu n2
je znázorněn schématem na obrázku 1.
proměnná n1
má celočíselnou hodnotu, ale proměnná n2
obsahuje odkaz na objekt a je to objekt, který drží celočíselnou hodnotu. Kromě toho objekt odkazovaný n2
obsahuje také odkaz na objekt třídy Double
.
problém s primitivy
než se vás pokusím přesvědčit o potřebě primitivních typů, měl bych uznat, že mnoho lidí se mnou nebude souhlasit. Sherman Alpert v „primitivních typech považovaných za škodlivé“ tvrdí, že primitivy jsou škodlivé, protože mísí “ procedurální sémantiku do jinak jednotného objektově orientovaného modelu. Primitiva nejsou prvotřídní objekty, přesto existují v jazyce, který zahrnuje především prvotřídní objekty.“Primitivy a objekty (ve formě tříd obalů) poskytují dva způsoby manipulace s logicky podobnými typy, ale mají velmi odlišnou základní sémantiku. Například, jak by měly být porovnány dva případy pro rovnost? Pro primitivní typy se používá operátor ==
, ale pro objekty je preferovanou volbou volání metody equals()
, což není volba pro primitivy. Podobně existují různé sémantiky při přiřazování hodnot nebo předávání parametrů. 0
pro int
oproti null
pro Integer
.
více informací o této problematice naleznete v blogu Erica Bruna „a modern primitive discussion“, který shrnuje některé výhody a nevýhody primitivů. Řada diskusí o přetečení zásobníku se také zaměřuje na primitivy, včetně „proč lidé stále používají primitivní typy v Javě?“a“ existuje důvod vždy používat objekty místo primitiv?.“Programátoři Stack Exchange hostí podobnou diskusi s názvem“ kdy použít primitivní třídu vs v Javě?“.
využití paměti
a double
v Javě vždy zabírá 64 bitů v paměti, ale velikost odkazu závisí na virtuálním stroji Java (JVM). Můj počítač spouští 64bitovou verzi systému Windows 7 a 64bitový JVM, a proto odkaz na můj počítač zabírá 64 bitů. Na základě diagramu na obrázku 1 bych očekával, že jeden double
, jako je n1
, obsadí 8 bajtů (64 bitů) a očekával bych, že jeden Double
, jako je n2
, obsadí 24 bajtů-8 Pro odkaz na objekt, 8 pro hodnotu double
uloženou v objektu a 8 Pro odkaz na objekt třídy pro Double
. Navíc Java používá další paměť pro podporu sběru odpadků pro typy objektů, ale ne pro primitivní typy. Pojďme se na to podívat.
použití přístupu podobného přístupu Glen McCluskey v “ Java primitive types vs. wrappers, “ metoda uvedená v seznamu 1 měří počet bajtů obsazených maticí n-by-n (dvourozměrné pole) double
.
Výpis 1. Výpočet využití paměti typu double
úpravou kódu v seznamu 1 se zřejmými změnami typu (nezobrazeno) můžeme také změřit počet bajtů obsazených maticí n-by-n Double
. Když testuji tyto dvě metody na svém počítači pomocí matic 1000 x 1000, získám výsledky uvedené v tabulce 1 níže. Jak je znázorněno, verze pro primitivní typ double
se rovná o něco více než 8 bajtů na položku v matici, zhruba to, co jsem očekával. Verze pro typ objektu Double
však vyžadovala o něco více než 28 bajtů na položku v matici. V tomto případě je tedy využití paměti Double
více než trojnásobek využití paměti double
, což by nemělo být překvapením pro každého, kdo rozumí rozložení paměti znázorněnému na obrázku 1 výše.
Runtime performance
pro porovnání runtime performancí pro primitivy a objekty potřebujeme algoritmus, kterému dominují numerické výpočty. Pro tento článek jsem zvolil násobení matic a vypočítám čas potřebný k násobení dvou matic 1000 x 1000. Kódoval jsem násobení matice pro double
přímým způsobem, jak je uvedeno v seznamu 2 níže. I když mohou existovat rychlejší způsoby, jak implementovat násobení matic (možná pomocí souběžnosti), tento bod není pro tento článek skutečně relevantní. Vše, co potřebuji, je společný kód ve dvou podobných metodách, jeden pomocí primitivní double
a jeden pomocí třídy wrapper Double
. Kód pro násobení dvou matic typu Double
je přesně stejný jako v seznamu 2 se zřejmými změnami typu.
Výpis 2. Vynásobením dvou matic typu double
jsem obě metody několikrát vynásobil dvěma maticemi 1000 x 1000 v počítači a změřil výsledky. Průměrné časy jsou uvedeny v tabulce 2. V tomto případě je tedy běhový výkon double
více než čtyřikrát rychlejší než výkon Double
. To je prostě příliš velký rozdíl ignorovat.
scimark 2.0 benchmark
zatím jsem použil jediný, jednoduchý benchmark násobení matic, abych prokázal, že Primitiva mohou přinést výrazně vyšší výpočetní výkon než objekty. K posílení svých tvrzení použiji vědeckovýzkumnější měřítko. SciMark 2.0 je Java měřítko pro vědecké a numerické výpočty k dispozici od Národního institutu pro standardy a technologie (NIST). Stáhl jsem zdrojový kód pro tento benchmark a vytvořil dvě verze, původní verzi pomocí primitiv a druhou verzi pomocí tříd wrapperů. Pro druhou verzi jsem nahradil int
Integer
a double
Double
, abych získal plný efekt použití tříd obalů. Obě verze jsou k dispozici ve zdrojovém kódu tohoto článku.
scimark benchmark měří výkon několika výpočetních rutin a hlásí kompozitní skóre v přibližných Mflops (miliony operací s plovoucí desetinnou čárkou za sekundu). Větší čísla jsou tedy pro tento benchmark lepší. Tabulka 3 uvádí průměrné složené skóre z několika běhů každé verze tohoto benchmarku v mém počítači. Jak je ukázáno, běhové výkony obou verzí scimark 2.0 benchmarku byly v souladu s výsledky násobení matice výše v tom, že verze s primitivy byla téměř pětkrát rychlejší než verze používající třídy wrapper.
viděli jste několik variant Java programů, které provádějí numerické výpočty, a to jak pomocí domácího benchmarku, tak vědeckého. Jak se však Java porovnává s jinými jazyky? Na závěr se rychle podívám na to, jak se výkon Java srovnává s výkonem tří dalších programovacích jazyků: Scala, C++ a JavaScript.
Benchmarking Scala
Scala je programovací jazyk, který běží na JVM a zdá se, že získává na popularitě. Scala má jednotný typový systém, což znamená, že nerozlišuje mezi primitivy a objekty. Podle Erika Osheima v číselné třídě Scala (Pt. 1), Scala používá primitivní typy, pokud je to možné, ale v případě potřeby použije objekty. Podobně, Martin Odersky popis Scala pole říká, že“… Scala pole Array
je reprezentováno jako Java int
, Array
je reprezentováno jako Java double
…“
znamená to tedy, že systém sjednoceného typu Scala bude mít runtime výkon srovnatelný s primitivními typy Java? Podívejme se.
nejsem se Scalou tak zdatný jako s Javou, ale pokusil jsem se převést kód pro maticový multiplikační benchmark přímo z Javy na Scalu. Výsledek je uveden v seznamu 3 níže. Když jsem na svém počítači spustil verzi benchmarku Scala, v průměru to bylo 12.30 sekund, což dává výkon Scaly velmi blízko k výkonu Java s primitivy. Tento výsledek je mnohem lepší, než jsem čekal, a podporuje tvrzení o tom, jak Scala zpracovává číselné typy.
Výpis 3. Vynásobením dvou matic v Scala
Benchmarking C++
protože C++ běží přímo na“ holém kovu “ spíše než ve virtuálním stroji, dalo by se přirozeně očekávat, že C++ bude běžet rychleji než Java. Výkon Java je navíc mírně snížen skutečností, že Java kontroluje přístup k polím, aby se zajistilo, že každý index je v mezích deklarovaných pro pole, zatímco C++ ne (funkce C++, která může vést k přetečení vyrovnávací paměti, kterou mohou hackeři využít). Zjistil jsem, že C++ je poněkud trapnější než Java při řešení základních dvourozměrných polí, ale naštěstí může být velká část této trapnosti skryta uvnitř soukromých částí třídy. Pro c++ jsem vytvořil jednoduchou verzi třídy Matrix
a přetížil jsem operátor *
pro násobení dvou matic, ale základní algoritmus násobení matic byl převeden přímo z verze Java. Zdrojový kód C++ je uveden v seznamu 4.
Výpis 4. Vynásobením dvou matic v C++
pomocí Eclipse CDT (Eclipse pro vývojáře C++) s kompilátorem MinGW C++ je možné vytvořit ladicí i uvolňovací verze aplikace. Pro testování C++ jsem několikrát spustil verzi vydání a zprůměroval výsledky. Jak se očekávalo, C++ běžel znatelně rychleji na tomto jednoduchém benchmarku, v průměru 7.58 sekund na mém počítači. Pokud je surový výkon primárním faktorem pro výběr programovacího jazyka, pak C++ je jazyk volby pro numericky náročné aplikace.
Benchmarking JavaScript
dobře, tohle mě překvapilo. Vzhledem k tomu, že JavaScript je velmi dynamický jazyk, očekával jsem, že jeho výkon bude nejhorší ze všech, dokonce horší než Java s třídami obalů. Ale ve skutečnosti byl výkon JavaScriptu mnohem blíže výkonu Javy s primitivy. Pro testování JavaScriptu jsem nainstaloval uzel.js, JavaScript engine s pověstí velmi efektivní. Výsledky byly v průměru 15,91 sekundy. Výpis 5 ukazuje verzi JavaScriptu maticového multiplikačního benchmarku, který jsem běžel na uzlu.js
Výpis 5. Násobení dvou matic v JavaScriptu
na závěr
když Java poprvé přišla na scénu před 18 lety, nebyl to nejlepší jazyk z hlediska výkonu pro aplikace, kterým dominují numerické výpočty. Ale v průběhu času, s technologickým pokrokem v oblastech, jako je kompilace just-in-time (jit) (aka adaptive nebo dynamic compilation), je výkon Java pro tyto druhy aplikací nyní srovnatelný s výkonem jazyků, které jsou kompilovány do nativního kódu při použití primitivních typů.
navíc Primitiva eliminují potřebu sběru odpadků, čímž poskytují další výkonnostní výhodu primitiv oproti typům objektů. Tabulka 4 shrnuje běhový výkon benchmarku pro násobení matice v mém počítači. Další faktory, jako je udržovatelnost, přenositelnost a odborné znalosti vývojářů, činí Javu lepší volbou pro mnoho takových aplikací.
jak již bylo dříve diskutováno, zdá se, že Oracle vážně zvažuje odstranění primitiv v budoucí verzi Java. Pokud kompilátor Java nedokáže generovat kód s výkonem srovnatelným s výkonem primitiv, myslím si, že jejich odstranění z Javy by vylučovalo použití Javy pro určité třídy aplikací; konkrétně ty aplikace, kterým dominují numerické výpočty. V tomto článku jsem použil jednoduchý benchmark založený na násobení matic a vědečtější benchmark, SciMark 2.0, abych argumentoval tímto bodem.
o autorovi
John I. Moore, Jr., Profesor matematiky a informatiky na Citadele, má širokou škálu zkušeností v průmyslu i akademické sféře, se specifickými odbornými znalostmi v oblasti objektově orientované technologie, softwarové inženýrství, a aplikovaná matematika. Již více než tři desetiletí navrhuje a vyvíjí software využívající relační databáze a několik jazyků vysokého řádu a od Verze 1.1 intenzivně pracuje v Javě. Kromě toho vyvinul a vyučoval řadu akademických kurzů a průmyslových seminářů o pokročilých tématech v informatice.
další čtení
- Paul Krill napsal o dlouhodobých plánech společnosti Oracle pro Javu v „Oracle stanoví záměry Java s dlouhým dosahem“ (JavaWorld, březen 2012). Tento článek, spolu s přidruženým vláknem komentářů, motivoval mě k napsání této obrany primitivů.
- Szymon Guz píše o svých výsledcích v benchmarkingu primitivních typů a tříd obalů v „Primitives and objects benchmark in Java“ (SimonOnSoftware, leden 2011).
- na webových stránkách podpory pro programování — principy a praxe pomocí C++ (Addison-Wesley, 2009) poskytuje tvůrce C++ Bjarne Stroustrup implementaci pro maticovou třídu, která je mnohem úplnější než ta, která doprovází tento článek.
- John Rose, Brian Goetz a Guy Steele diskutují o konceptu nazvaném typy hodnot v „stavu hodnot“ (OpenJDK.net, duben 2014). Hodnotové typy lze považovat za neměnné uživatelem definované agregátní typy bez identity, které kombinují vlastnosti objektů i primitiv. Mantra pro typy hodnot je “ kódy jako třída, funguje jako int.“