Programovanie

Zrýchlite Javu: Optimalizujte!

Podľa priekopníckeho počítačového vedca Donalda Knutha „predčasná optimalizácia je koreňom všetkého zla“. Každý článok o optimalizácii musí začať poukázaním na to, že dôvodov je zvyčajne viac nie optimalizovať ako optimalizovať.

  • Ak váš kód už funguje, optimalizácia je bezpečný spôsob, ako zaviesť nové a možno aj jemné chyby

  • Optimalizácia vedie k sťaženiu porozumenia a údržby kódu

  • Niektoré z tu prezentovaných techník zvyšujú rýchlosť znižovaním rozšíriteľnosti kódu

  • Optimalizácia kódu pre jednu platformu môže na inej platforme skutočne zhoršiť

  • Veľa času možno venovať optimalizácii, s malým zvýšením výkonu a môže viesť k zahmlievaniu kódu

  • Ak ste príliš posadnutí optimalizačným kódom, ľudia vás za chrbtom označia za pitomca

Pred optimalizáciou by ste mali starostlivo zvážiť, či optimalizáciu vôbec potrebujete. Optimalizácia v prostredí Java môže byť neuchopiteľným cieľom, pretože prostredie na vykonávanie sa líši. Použitie lepšieho algoritmu pravdepodobne prinesie väčšie zvýšenie výkonu ako akékoľvek množstvo nízkoúrovňových optimalizácií a je pravdepodobnejšie, že prinesie zlepšenie za všetkých podmienok vykonávania. Pred vykonaním nízkoúrovňovej optimalizácie by sa spravidla malo zvážiť optimalizovanie na vysokej úrovni.

Prečo teda optimalizovať?

Ak je to taký zlý nápad, prečo vôbec optimalizovať? No v ideálnom svete by ste to neurobili. Realita je však taká, že niekedy je najväčším problémom programu to, že vyžaduje jednoducho príliš veľa zdrojov, a tieto zdroje (pamäť, cykly CPU, šírka pásma siete alebo ich kombinácia) môžu byť obmedzené. Fragmenty kódu, ktoré sa v programe vyskytujú viackrát, budú pravdepodobne citlivé na veľkosť, zatiaľ čo kód s mnohými iteráciami vykonania môže byť citlivý na rýchlosť.

Zrýchlite Javu!

Ako interpretovaný jazyk s kompaktným bytecode je v jazyku Java najčastejšie problém s rýchlosťou alebo jeho nedostatkom. Primárne sa zameriame na to, ako zabezpečiť, aby Java bežala rýchlejšie, než aby sa zmestila do menšieho priestoru - hoci upozorníme, kde a ako tieto prístupy ovplyvňujú pamäť alebo šírku pásma siete. Dôraz sa bude klásť skôr na hlavný jazyk ako na Java API.

Mimochodom, jedna vec sme nebude tu je diskutované použitie natívnych metód napísaných v C alebo assembly. Aj keď použitie natívnych metód môže priniesť konečné zvýšenie výkonu, robí to za cenu nezávislosti platformy Java. Pre vybrané platformy je možné napísať ako Java verziu metódy, tak aj natívne verzie; to vedie k zvýšeniu výkonu na niektorých platformách bez toho, aby sa vzdali možnosti fungovať na všetkých platformách. Ale to je všetko, čo k téme nahradenia Javy kódom C poviem. (Viac informácií o tejto téme nájdete v Tipu pre Java, „Písanie natívnych metód“.) V tomto článku sa zameriavame na to, ako urobiť jazyk Java rýchlym.

90/10, 80/20, chata, chata, túra!

Spravidla sa 90 percent času vylúčenia programu strávi vykonaním 10 percent kódu. (Niektorí ľudia používajú pravidlo 80 percent / 20 percent, ale moje skúsenosti s písaním a optimalizáciou komerčných hier vo viacerých jazykoch za posledných 15 rokov ukázali, že vzorec 90 percent / 10 percent je typický pre programy náročné na výkon, pretože len málo úloh má tendenciu byť vykonávané s vysokou frekvenciou.) Optimalizácia ostatných 90 percent programu (kde bolo použitých 10 percent času vykonania) nemá na výkon znateľný vplyv. Keby sa vám podarilo dosiahnuť, aby sa 90 percent kódu spustilo dvakrát rýchlejšie, program by bol len o 5 percent rýchlejší. Prvou úlohou pri optimalizácii kódu je teda identifikovať 10 percent (často je to menej) programu, ktorý spotrebuje väčšinu času vykonania. Nie vždy to je miesto, kde čakáte.

Všeobecné optimalizačné techniky

Existuje niekoľko bežných optimalizačných techník, ktoré sa uplatňujú bez ohľadu na použitý jazyk. Niektoré z týchto techník, napríklad prideľovanie globálnych registrov, sú prepracované stratégie prideľovania prostriedkov stroja (napríklad registre CPU) a nevzťahujú sa na bajtové kódy Java. Zameriame sa na techniky, ktoré v zásade zahŕňajú reštrukturalizáciu kódu a nahradenie ekvivalentných operácií v rámci metódy.

Zníženie sily

Zníženie sily nastane, keď je operácia nahradená ekvivalentnou operáciou, ktorá sa vykoná rýchlejšie. Najbežnejším príkladom zníženia sily je použitie operátora posunu na násobenie a delenie celých čísel silou 2. Napríklad x >> 2 možno použiť namiesto x / 4a x << 1 nahrádza x * 2.

Spoločná eliminácia podvýrazu

Odstránenie spoločného podvýrazu odstráni nadbytočné výpočty. Namiesto písania

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

spoločný podvýraz sa počíta raz a používa sa pre oba výpočty:

dvojnásobná hĺbka = d * (lim / max); dvojité x = hĺbka * sx; dvojité y = hĺbka * sy;

Návrh kódu

Pohyb kódu presunie kód, ktorý vykoná operáciu alebo vypočíta výraz, ktorého výsledok sa nezmení alebo nie je nemenný. Kód sa presunie tak, že sa vykoná iba vtedy, keď sa môže zmeniť výsledok, a nie vykonať ho vždy, keď sa vyžaduje výsledok. Je to najbežnejšie pri cykloch, ale môže to zahŕňať aj kód opakovaný pri každom vyvolaní metódy. Nasleduje príklad invariantného pohybu kódu v slučke:

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

sa stáva

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

Odvíjanie slučiek

Odvíjanie slučiek znižuje réžiu riadiaceho kódu slučky tým, že zakaždým vykonáva cez slučku viac ako jednu operáciu a následne vykonáva menej iterácií. Podľa predchádzajúceho príkladu, ak vieme, že dĺžka X[] je vždy násobok dvoch, môžeme slučku prepísať ako:

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

V praxi takéto rozvinutie slučiek - v ktorých sa hodnota indexu slučky používa v rámci slučky a musí sa osobitne zvyšovať - ​​neprináša znateľné zvýšenie rýchlosti interpretovanej Javy, pretože v bytecodoch chýbajú pokyny na efektívne kombinovanie „+1"do indexu poľa.

Všetky tipy na optimalizáciu v tomto článku obsahujú jednu alebo viac všeobecných techník uvedených vyššie.

Uvedenie kompilátora do činnosti

Moderné kompilátory C a Fortran vytvárajú vysoko optimalizovaný kód. Kompilátory C ++ zvyčajne produkujú menej efektívny kód, ale stále sú na dobrej ceste k produkcii optimálneho kódu. Všetci títo kompilátori prešli mnohými generáciami pod vplyvom silnej konkurencie na trhu a stali sa jemne zdokonalenými nástrojmi na vytlačenie každej poslednej kvapky výkonu z bežného kódu. Takmer určite používajú všetky vyššie uvedené všeobecné optimalizačné techniky. Stále však zostáva veľa trikov na to, aby kompilátory vygenerovali efektívny kód.

kompilátory javac, JIT a natívne kódy

Úroveň optimalizácie javac vykonáva pri kompilácii kódu v tomto okamihu je minimálna. Predvolene robí nasledovné:

  • Konštantné skladanie - kompilátor rieši všetky konštantné výrazy také, že i = (10 * 10) zostavuje do i = 100.

  • Skladanie konárov (väčšinou) - zbytočné ísť do bytecodes sa vyhnú.

  • Obmedzená eliminácia mŕtveho kódu - pre výroky typu sa nevyrába žiadny kód if (false) i = 1.

Úroveň optimalizácie, ktorú poskytuje javac, by sa mala zlepšiť, pravdepodobne dramaticky, ako jazyk dozrieva a dodávatelia kompilátorov začnú seriózne súťažiť na základe generovania kódu. Java práve teraz získava kompilátory druhej generácie.

Potom existujú aj kompilátory just-in-time (JIT), ktoré za behu prevádzajú bajtové kódy Java na natívny kód. Niektoré sú už k dispozícii a hoci môžu dramaticky zvýšiť rýchlosť vykonávania vášho programu, úroveň optimalizácie, ktorú môžu vykonať, je obmedzená, pretože k optimalizácii dochádza za behu programu. Kompilátor JIT sa viac zaoberá generovaním kódu rýchlo ako generovaním najrýchlejšieho kódu.

Prekladače natívnych kódov, ktoré kompilujú Javu priamo do natívneho kódu, by mali ponúkať najväčší výkon, ale za cenu nezávislosti na platforme. Našťastie veľa tu prezentovaných trikov bude dosiahnuteľných budúcimi kompilátormi, ale zatiaľ je potrebné vykonať trochu práce, aby ste kompilátor vyťažili čo najviac.

javac ponúka jednu z možností výkonu, ktorú môžete povoliť: vyvolanie súboru -O možnosť spôsobiť, že kompilátor vloží určité volania metód:

javac -O MyClass

Inlinovanie volania metódy vloží kód metódy priamo do kódu uskutočňujúceho volanie metódy. To eliminuje réžiu volania metódy. Pre malú metódu môže táto réžia predstavovať významné percento jej času vykonania. Upozorňujeme, že iba metódy deklarované ako buď súkromné, statickýalebo konečné možno považovať za vloženie, pretože iba tieto metódy kompilátor staticky vyriešil. Tiež synchronizované metódy nebudú vložené. Kompilátor vloží iba malé metódy, ktoré sa zvyčajne skladajú iba z jedného alebo dvoch riadkov kódu.

Bohužiaľ verzie 1.0 kompilátora javac majú chybu, ktorá vygeneruje kód, ktorý nedokáže prejsť overovačom bytecode, keď -O možnosť je použitá. To bolo opravené v JDK 1.1. (Overovateľ bytecode overí kód pred jeho spustením, aby sa ubezpečil, že neporušuje žiadne pravidlá Javy.) Zaradí metódy, ktoré odkazujú na členov triedy, ktorí sú pre volajúcu triedu neprístupní. Napríklad, ak sú nasledujúce triedy zostavené spoločne pomocou znaku -O možnosť

trieda A {private static int x = 10; public static void getX () {return x; }} trieda B {int y = A.getX (); } 

volanie A.getX () v triede B bude v triede B zarovnané tak, akoby B bolo napísané ako:

trieda B {int y = A.x; } 

To však spôsobí, že generovanie bytových kódov bude mať prístup k súkromnej premennej A.x, ktorá sa vygeneruje v kóde B. Tento kód sa vykoná dobre, ale pretože porušuje obmedzenia prístupu Java, overovateľ ho označí znakom IllegalAccessError pri prvom spustení kódu.

Táto chyba nerobí -O možnosť zbytočná, musíte si však dať pozor, ako ju použijete. Ak je vyvolané v jednej triede, môže bez rizika vložiť určité volania metód v rámci triedy. Niekoľko tried môže byť spolu podriadených, pokiaľ neexistujú potenciálne obmedzenia prístupu. Niektoré kódy (napríklad aplikácie) nepodliehajú overovaču bytecode. Túto chybu môžete ignorovať, ak viete, že sa váš kód spustí iba bez toho, aby bol podrobený overovaču. Ďalšie informácie nájdete v mojich častých dotazoch k javac-O.

Profiléri

Našťastie JDK prichádza so zabudovaným profilovačom, ktorý pomáha identifikovať, kde sa v programe trávi čas. Bude sledovať čas strávený každou rutinou a zapíše informácie do súboru java.prof. Ak chcete spustiť profiler, použite -prof pri vyvolaní tlmočníka Java:

java -profil myClass

Alebo na použitie s appletom:

java -prof sun.applet.AppletViewer myApplet.html

Existuje niekoľko upozornení na použitie profilovača. Výstup profilovača nie je obzvlášť ľahké dešifrovať. V JDK 1.0.2 taktiež skráti názvy metód na 30 znakov, takže nemusí byť možné rozlišovať niektoré metódy. Bohužiaľ, v prípade systému Mac neexistuje žiadny prostriedok na vyvolanie profilovača, takže používatelia počítačov Mac majú smolu. Okrem toho, stránka dokumentu Java spoločnosti Sun (pozri Zdroje) už neobsahuje dokumentáciu pre server -prof možnosť). Ak však vaša platforma podporuje -prof pri interpretácii výsledkov môžete použiť buď HyperProf Vladimíra Bulatova, alebo ProfilViewer Grega Whitea (pozri Zdroje).

Je tiež možné „profilovať“ kód vložením explicitného načasovania do kódu:

dlhý štart = System.currentTimeMillis (); // urobí sa tu časovaná operácia = System.currentTimeMillis () - štart;

System.currentTimeMillis () vráti čas za 1/1 000 sekundy. Niektoré systémy, napríklad Windows PC, však majú systémový časovač s menším (oveľa menším) rozlíšením ako 1/1 000 sekundy. Ani 1/1 000 sekundy nie je dosť dlhá na presné načasovanie mnohých operácií. V týchto prípadoch alebo v systémoch s časovačmi s nízkym rozlíšením môže byť potrebné načasovať, ako dlho trvá opakovanie operácie n krát a potom vydelíme celkový čas n získať skutočný čas. Aj keď je k dispozícii profilovanie, môže byť táto technika užitočná na načasovanie konkrétnej úlohy alebo operácie.

Tu uvádzam niekoľko záverečných poznámok k profilovaniu:

  • Pred a po vykonaní zmien vždy načasujte kód, aby ste si overili, že vaše zmeny vylepšili program aspoň na testovacej platforme

  • Pokúste sa každú skúšku časovania vykonať za rovnakých podmienok

  • Ak je to možné, vykonajte test, ktorý sa nespolieha na žiadny vstup používateľa, pretože variácie v reakcii používateľov môžu spôsobiť kolísanie výsledkov

Benchmarkový applet

Benchmarkový applet meria čas potrebný na vykonanie operácie tisíckrát (alebo dokonca milióny), odpočíta čas strávený vykonaním iných operácií ako testu (napríklad réžia slučky) a potom z týchto informácií vypočítava, ako dlho sú jednotlivé operácie vzal. Každý test prebieha približne jednu sekundu. V snahe eliminovať náhodné oneskorenia z iných operácií, ktoré môže počítač vykonať počas testu, vykoná každý test trikrát a použije najlepší výsledok. Pokúša sa tiež vylúčiť zber odpadu ako faktor pri testoch. Z tohto dôvodu, čím viac pamäte je k dispozícii pre test, tým sú výsledky testu presnejšie.

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