Programování

Zrychlete Javu: Optimalizujte!

Podle průkopnického počítačového vědce Donalda Knutha „Předčasná optimalizace je kořenem všeho zla.“ Jakýkoli článek o optimalizaci musí začít zdůrazněním, že důvodů je obvykle více ne optimalizovat než optimalizovat.

  • Pokud váš kód již funguje, jeho optimalizace je jistým způsobem, jak zavést nové a případně jemné chyby

  • Optimalizace má tendenci dělat kód těžší pochopit a udržovat

  • Některé zde prezentované techniky zvyšují rychlost snížením rozšiřitelnosti kódu

  • Optimalizace kódu pro jednu platformu může ve skutečnosti na jiné platformě zhoršit

  • Hodně času lze strávit optimalizací, s malým ziskem na výkonu, a může to mít za následek zmatený kód

  • Pokud jste příliš posedlí optimalizací kódu, lidé vám budou říkat blbeček za zády

Před optimalizací byste měli pečlivě zvážit, zda vůbec potřebujete optimalizovat. Optimalizace v Javě může být nepolapitelným cílem, protože prostředí pro provádění se liší. Použití lepšího algoritmu pravděpodobně přinese větší zvýšení výkonu než jakékoli množství nízkoúrovňových optimalizací a je pravděpodobnější, že přinese zlepšení za všech podmínek provádění. Před provedením nízkoúrovňové optimalizace je zpravidla třeba zvážit optimalizaci na vysoké úrovni.

Proč tedy optimalizovat?

Pokud je to tak špatný nápad, proč vůbec optimalizovat? V ideálním světě byste ne. Realita je však taková, že někdy je největším problémem programu to, že vyžaduje prostě příliš mnoho zdrojů, a tyto zdroje (paměť, cykly CPU, šířka pásma sítě nebo jejich kombinace) mohou být omezené. Fragmenty kódu, které se vyskytnou vícekrát v celém programu, budou pravděpodobně citlivé na velikost, zatímco kód s mnoha iteracemi spuštění může být citlivý na rychlost.

Udělejte Javu rychle!

Jako interpretovaný jazyk s kompaktním bytovým kódem se v Javě jako problém nejčastěji objevuje rychlost nebo jeho nedostatek. Primárně se podíváme na to, jak zajistit, aby Java běžela rychleji, než aby se vešla do menšího prostoru - i když ukážeme, kde a jak tyto přístupy ovlivňují paměť nebo šířku pásma sítě. Důraz bude kladen spíše na základní jazyk než na rozhraní Java API.

Mimochodem, jedna věc je zvyklý diskutovat zde je použití nativních metod napsaných v C nebo assembleru. I když použití nativních metod může přinést maximální zvýšení výkonu, činí to za cenu nezávislosti platformy Java. Pro vybrané platformy je možné napsat jak Java verzi metody, tak nativní verze; to vede ke zvýšení výkonu na některých platformách, aniž bychom se vzdali schopnosti běžet na všech platformách. Ale to je vše, co řeknu na téma nahrazení Javy kódem C. (Další informace o tomto tématu najdete v tipu Java „Psaní nativních metod“.) V tomto článku se zaměřujeme na to, jak zrychlit prostředí Java.

90/10, 80/20, chata, chata, výlet!

Je pravidlem, že 90 procent excitační doby programu je vynaloženo na provedení 10 procent kódu. (Někteří lidé používají pravidlo 80 procent / 20 procent, ale moje zkušenosti s psaním a optimalizací komerčních her v několika jazycích za posledních 15 let ukázaly, že vzorec 90 procent / 10 procent je typický pro programy náročné na výkon, protože jen málo úkolů má tendenci být prováděno s velkou frekvencí.) Optimalizace dalších 90 procent programu (kde bylo vyčerpáno 10 procent času provádění) nemá na výkon znatelný vliv. Pokud byste dokázali zajistit, aby se 90 procent kódu spustilo dvakrát rychleji, program by byl jen o 5 procent rychlejší. Prvním úkolem při optimalizaci kódu je tedy identifikace 10 procent (často je to méně) programu, který spotřebovává většinu času provádění. To není vždy tam, kde to očekáváte.

Obecné optimalizační techniky

Existuje několik běžných optimalizačních technik, které platí bez ohledu na použitý jazyk. Některé z těchto technik, například globální alokace registrů, jsou sofistikované strategie pro alokaci prostředků stroje (například registry CPU) a nevztahují se na bajtové kódy Java. Zaměříme se na techniky, které v zásadě zahrnují restrukturalizaci kódu a nahrazení ekvivalentních operací v rámci metody.

Snížení síly

Snížení síly nastane, když je operace nahrazena ekvivalentní operací, která se provádí rychleji. Nejběžnějším příkladem snížení síly je použití operátoru posunu k násobení a dělení celých čísel mocí 2. Například x >> 2 lze použít místo x / 4, a x << 1 nahrazuje x * 2.

Běžná eliminace dílčích výrazů

Společná eliminace dílčích výrazů odstraní nadbytečné výpočty. Místo psaní

double x = d * (lim / max) * sx; dvojité y = d * (lim / max) * sy;

společný dílčí výraz se vypočítá jednou a použije se pro oba výpočty:

dvojitá hloubka = d * (lim / max); double x = hloubka * sx; dvojité y = hloubka * sy;

Pohyb kódu

Pohyb kódu přesune kód, který provede operaci nebo vypočítá výraz, jehož výsledek se nezmění nebo je neměnný. Kód se přesune tak, že se provede pouze tehdy, když se může změnit výsledek, místo aby se provedl pokaždé, když je výsledek požadován. To je nejběžnější u smyček, ale může to také zahrnovat kód opakovaný při každém vyvolání metody. Následuje příklad invariantního pohybu kódu ve smyčce:

pro (int i = 0; i <x.length; i ++) x [i] * = Math.PI * Math.cos (y); 

se stává

double picosy = Math.PI * Math.cos (y);pro (int i = 0; i <x.length; i ++) x [i] * = pikikosy; 

Odvíjení smyček

Odvíjení smyček snižuje režii řídicího kódu smyčky tím, že pokaždé provede smyčku více než jednou operací a následně provede méně iterací. Práce z předchozího příkladu, pokud víme, že délka X[] je vždy násobkem dvou, můžeme smyčku přepsat jako:

double picosy = Math.PI * Math.cos (y);pro (int i = 0; i <x.length; i + = 2) { x [i] * = pikikosy; x [i + 1] * = pikikosy; } 

V praxi odvíjení smyček, jako je tato - ve kterých se hodnota indexu smyčky používá ve smyčce a musí být samostatně zvyšována - nepřináší znatelné zvýšení rychlosti v interpretované Javě, protože v bytových kódech chybí pokyny k efektivní kombinaci "+1"do indexu pole.

Všechny optimalizační tipy v tomto článku obsahují jednu nebo více obecných technik uvedených výše.

Uvedení kompilátoru do provozu

Moderní překladače C a Fortran produkují vysoce optimalizovaný kód. C ++ kompilátory obecně produkují méně efektivní kód, ale stále jsou na dobré cestě k produkci optimálního kódu. Všechny tyto kompilátory prošly mnoha generacemi pod vlivem silné tržní konkurence a staly se jemně vylepšenými nástroji pro vytlačování každé poslední kapky výkonu z běžného kódu. Téměř jistě používají všechny výše uvedené obecné optimalizační techniky. Stále ale zbývá spousta triků, jak kompilátory vygenerovat efektivní kód.

překladače javac, JIT a nativní kód

Úroveň optimalizace javac provádí při kompilaci kódu v tomto okamžiku je minimální. Výchozí nastavení je následující:

  • Konstantní skládání - kompilátor řeší všechny konstantní výrazy tak, že i = (10 * 10) sestavuje do i = 100.

  • Skládání větví (většinou) - zbytečné jít do bytecodes jsou vyloučeny.

  • Omezená eliminace mrtvého kódu - pro příkazy typu se neprodukuje žádný kód if (false) i = 1.

Úroveň optimalizace, kterou poskytuje javac, by se měla zlepšit, pravděpodobně dramaticky, jak jazyk dozrává a prodejci překladačů začínají seriózně soutěžit na základě generování kódu. Java právě teď získává kompilátory druhé generace.

Pak existují kompilátory just-in-time (JIT), které za běhu převádějí bajtové kódy Java do nativního kódu. Některé jsou již k dispozici a zatímco mohou dramaticky zvýšit rychlost provádění vašeho programu, úroveň optimalizace, kterou mohou provádět, je omezena, protože k optimalizaci dochází za běhu. Kompilátor JIT se více zabývá rychlým generováním kódu než generováním nejrychlejšího kódu.

Překladače nativního kódu, které kompilují Javu přímo do nativního kódu, by měly nabídnout nejvyšší výkon, ale za cenu nezávislosti na platformě. Naštěstí mnoho zde prezentovaných triků bude dosaženo budoucími kompilátory, ale prozatím trvá trochu práce, než kompilátor vytěžit co nejvíce.

javac nabízí jednu možnost výkonu, kterou můžete povolit: vyvolání možnost způsobit, že kompilátor inline určité volání metody:

javac -O MyClass

Vložení volání metody vloží kód metody přímo do kódu, který volání metody provede. To eliminuje režii volání metody. U malé metody může tato režie představovat významné procento jejího času provedení. Všimněte si, že pouze metody deklarované jako buď soukromé, statickýnebo finále lze uvažovat pro vložení, protože pouze tyto metody jsou staticky vyřešeny kompilátorem. Taky, synchronizované metody nebudou vloženy. Kompilátor bude vkládat pouze malé metody, které se obvykle skládají pouze z jednoho nebo dvou řádků kódu.

Bohužel verze 1.0 kompilátoru javac mají chybu, která vygeneruje kód, který nemůže předat ověřovač bytecode, když je použita možnost. To bylo opraveno v JDK 1.1. (Verifikátor bytecode zkontroluje kód před jeho spuštěním, aby se ujistil, že neporušuje žádná pravidla Java.) Vloží metody, které odkazují na členy třídy nepřístupné pro volající třídu. Například pokud jsou následující třídy sestaveny společně pomocí volba

třída A {soukromý statický int x = 10; public static void getX () {return x; }} třída B {int y = A.getX (); } 

volání A.getX () ve třídě B bude ve třídě B inline, jako by B bylo zapsáno jako:

třída B {int y = A.x; } 

To však způsobí generování bajtových kódů pro přístup k soukromé proměnné A.x, která bude vygenerována v kódu B. Tento kód bude fungovat dobře, ale protože porušuje omezení přístupu Java, bude označen ověřovatelem znakem IllegalAccessError první spuštění kódu.

Tato chyba nedělá možnost k ničemu, ale musíte být opatrní, jak ji používáte. Pokud je vyvolána v jedné třídě, může bez rizika vložit určité volání metod v rámci třídy. Několik tříd může být vloženo společně, pokud neexistují žádná potenciální omezení přístupu. A některý kód (například aplikace) nepodléhá ověření bytecode. Tuto chybu můžete ignorovat, pokud víte, že se váš kód spustí pouze bez podrobení ověřovateli. Další informace najdete v mých nejčastějších dotazech k javac-O.

Profilery

Naštěstí JDK přichází s vestavěným profilerem, který pomáhá určit, kde se v programu tráví čas. Bude sledovat čas strávený v každé rutině a zapíše informace do souboru java.prof. Chcete-li spustit profiler, použijte -prof možnost při vyvolání tlumočníka Java:

java -profil myClass

Nebo pro použití s ​​appletem:

java -prof sun.applet.AppletViewer myApplet.html

Existuje několik upozornění pro použití profileru. Výstup profileru není zvlášť snadné dešifrovat. V JDK 1.0.2 také zkracuje názvy metod na 30 znaků, takže nemusí být možné rozlišit některé metody. Bohužel u počítačů Mac neexistuje žádný způsob, jak vyvolat profiler, takže uživatelé počítačů Mac nemají štěstí. Kromě toho stránka dokumentu Java společnosti Sun (viz Zdroje) již neobsahuje dokumentaci pro -prof volba). Pokud však vaše platforma podporuje -prof k interpretaci výsledků lze použít buď HyperProf Vladimíra Bulatova, nebo ProfilViewer Grega Whitea (viz Zdroje).

Je také možné „profilovat“ kód vložením explicitního načasování do kódu:

dlouhý start = System.currentTimeMillis (); // tu se má časovat operace = System.currentTimeMillis () - start;

System.currentTimeMillis () vrací čas za 1/1 000 sekundy. Některé systémy, například Windows PC, však mají systémový časovač s menším (mnohem menším) rozlišením než 1/1 000 sekundy. Ani 1/1 000 sekundy není dost dlouhá, aby přesně načasovala mnoho operací. V těchto případech nebo v systémech s časovačem s nízkým rozlišením může být nutné načasovat, jak dlouho trvá opakování operace n a poté vydělte celkový čas n získat skutečný čas. I když je k dispozici profilování, může být tato technika užitečná pro načasování konkrétního úkolu nebo operace.

Zde je několik závěrečných poznámek k profilování:

  • Vždy načasujte kód před a po provedení změn, abyste ověřili, že vaše změny vylepšily program alespoň na testovací platformě

  • Zkuste každou zkoušku časování provést za stejných podmínek

  • Pokud je to možné, vytvořte test, který se nespoléhá na žádný vstup uživatele, protože rozdíly v odezvě uživatele mohou způsobit kolísání výsledků

Benchmarkový applet

Benchmarkový applet měří čas potřebný k provedení operace tisíckrát (nebo dokonce miliony) krát, odečte čas strávený prováděním operací jiných než test (například režie smyčky) a poté pomocí těchto informací vypočítá, jak dlouho jednotlivé operace vzal. Spustí každý test přibližně jednu sekundu. Ve snaze eliminovat náhodné zpoždění z jiných operací, které může počítač během testu provést, provede každý test třikrát a použije nejlepší výsledek. Pokouší se také eliminovat uvolňování paměti jako faktor v testech. Z tohoto důvodu, čím více paměti je k dispozici pro benchmark, tím přesnější jsou výsledky benchmarku.

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