JE_37 - Optimalizujte rozumně

Existují tři aforizmy týkající se optimalizace, které by měl každý znát. Možná již začínají trpět přeexponovaností, pokud je ale náhodou neznáte, tady jsou:

Ve jménu efektivity (které není ani třeba dosaženo) se páchá více počítačových hříchů, než z jakéhokoli jiného důvodu - včetně naprosté tuposti.

- William A. Wulf

Malé nevýkonnosti bychom měli ignorovat řekněme v 97% případů: předčasná optimalizace je kořenem veškerého zla.

- Donald E. Knuth

Pokud se jedná o optimalizaci, řídíme se dvěma zásadami:

- M. A. Jackson

Všechny tyto aforizmy jsou o dvě desetiletí starší než programovací jazyk Java. O optimalizaci však říkají hlubokou pravdu: Je snadné způsobit víc škody než užitku, zejména pokud optimalizujete předčasně. V takovém procesu můžete vytvořit software, který není ani rychlý ani správný a ani jej nelze snadno opravit.

Neobětujte rozumné principy architektury výkonnosti. Snažte se psát dobré a nikoli rychlé programy. Není-li dobrý program dostatečně rychlý, pak bude možné jeho architekturu optimalizovat. Dobré programy napl�ují princip skrývání informací: kdykoli je to možné, lokalizují rozhodnutí návrhu v jednotlivých modulech, takže lze jednotlivá rozhodnutí změnit bez vlivu na zbytek systému (rada 12).

To neznamená, že můžete ignorovat nedostatečnou výkonnost až do dokončení programu. Implementující problémy lze opravit později optimalizací, ale pronikavé chyby architektury omezující výkonnost nemusejí být opravitelné bez přepsání systému. Pozdní změna zásadní stránky vašeho návrhu může mít z následek špatně strukturovaný systém, který se obtížně spravuje a vyvíjí. Proto byste měli myslet na výkonnost během procesu návrhu.

Snažte se vyhýbat rozhodnutím návrhu, která omezují výkonnost. Po vytvoření se nejobtížněji mění takové komponenty návrhu, které specifikují interakce mezi moduly a s okolním světem. Nejdůležitější mezi těmito komponentami návrhu jsou rozhraní API, protokoly na komunikační úrovni a stálé datové formáty. Nejenže je obtížné nebo nemožné tyto komponenty návrhu později změnit, ale každá z nich může výrazným způsobem omezovat výkonnost, jaké kdy může daný systém dosáhnout.

Zvažujte výkonnostní dopady svých rozhodnutí o návrhu API. Když učiníte nějaký veřejný typ měnitelným, může to vyžadovat spoustu zbytečného defenzivního kopírování (rada 24). Podobně použití dědičnosti ve třídě, kdy by byla kompozice vhodnější, navždy danou třídu váže k její nadtřídě, což může klást umělé limity na výkonnost podtřídy (rada 14). Konečně také používání implementačního typu namísto rozhraní v API vás váže ke konkrétní implementaci, třebaže se později mohou objevit rychlejší implementace (rada 34).

Vlivy návrhu API na výkonnost jsou velmi reálné. Zvažme metodu getSize ve třídě java.awt.Component. Rozhodnutí, že tato výkonnostně kritická metoda bude vracet instanci Dimension, společně s rozhodnutím, že instance Dimension jsou měnitelné, nutí každou implementaci této metody alokovat novou instanci Dimension při každém zavolání. Třebaže je alokování malých objektů ve verzi 1.3 relativně nenáročné, zbytečné alokování miliónů objektů může výkonnosti vážně uškodit.

V tomto případě existovalo několik alternativ. V ideálním případě měla být třída Dimension neměnitelná (rada 13); alternativně mohla být metoda getSize nahrazena dvěma metodami vracejícími jednotlivé primitivní součásti objektu Dimension. Ve skutečnosti byly do API Component přidány ve verzi 1.2 dvě takové metody kvůli výkonnosti. Již existující klientský kód však používá metodu getSize a nadále trpí výkonnostními důsledky původního rozhodnutí o návrhu API.

Naštěstí obvykle platí, že dobrý návrh API představuje také dobrou výkonnost. Je nesprávné pokroutit API v zájmu dosažení dobré výkonnosti. Výkonnostní problém, který vás přinutil ke změně, může zmizet v další verzi platformy nebo jiného využívaného softwaru, ale poničené API a problémy s jeho podporou které zapříči�uje, vás už nikdy neopustí.

Jakmile máte pečlivě navržený program a vytvoříte jasnou, stručnou a dobře dokumentovanou implementaci, pak můžete zvážit optimalizaci, pokud tedy již nejste spokojeni s výkonností svého programu. Vzpome�te si, že dvě Jacksonova pravidla optimalizace zněla: "Nedělejte ji" a "(pouze pro experty): Zatím ji nedělejte." Mohl přidat ještě jedno: Změřte výkonnost před a po každém pokusu o optimalizaci.

Můžete být překvapeni tím, co objevíte. Často nemají pokusy o optimalizaci žádný měřitelný vliv na výkonnost; někdy ji dokonce zhoršují. Hlavním důvodem je to, že je obtížné odhadnout, kde váš program tráví svůj čas. Ta část programu, o které si myslíte, že je pomalá, nemusí být na vině,čímž pak jenom ztrácíte čas, když se pokoušíte o její optimalizaci. Obecná moudrost říká, že program stráví 80% svého času ve 20% svého kódu.

Profilovací nástroje vám mohou pomoci určit, kam máte zaměřit svou snahu o optimalizaci. Takové nástroje vám poskytují informace o běhu, například kolik přibližně každá metoda spotřebovává času a kolikrát se volá. Kromě zaměření snahy o optimalizaci vás takový nástroj může upozornit na změnu algoritmů. Pokud se ve vašem programu skrývá nějaký kvadratický (nebo ještě horší) algoritmus, pak takový problém nevyřeší žádné ladění. Musíte daný algoritmus nahradit jiným, který je výkonnější. Čím více máte v systému kódu, tím je důležitější používat profilovací nástroj. Je to jako hledání jehly v kupce sena: Čím větší je ta kupka, tím užitečnější je detektor kovů. Java 2 SDK obsahuje jednoduchý profilovací nástroj a několik dalších a lepších je k dispozici komerčně.

Potřeba měřit vliv optimalizace je ještě výraznější na platformě Java než na tradičních platformách, protože programovací jazyk Java nemá silný výkonnostní model. Relativní náklady různých primitivních operací nejsou dobře definované. Sémanitcká mezera mezi tím, co programátor napíše, a tím, co CPU vykoná, je mnohem větší než v tradičních kompilovaných jazycích, což ztěžuje spolehlivou předpověď výkonnostních důsledků každé optimalizace. Existuje mnoho výkonnostních mýtů, které se ukazují jako pravdivé jen zpola nebo dokonce přímo jako lživé.

Nejenže je výkonnostní model chabě definovaný, ale také se liší mezi jednotlivými implementacemi JVM a jejich verzemi. Budete-li provozovat svůj program na více implementacích JVM, pak je důležité změřit vliv vaší optimalizace v každé z nich. Občas můžete být přinuceni k výkonnostním kompromisům na různých implementacích JVM.

Celkově můžeme říci, že byste se neměli snažit psát rychlé programy - snažte se vytvářet dobré programy; rychlost přijde sama. Přemýšlejte o výkonnostních problémech při navrhování systémů a zejména při navrhování API, protokolů na nízké úrovni a stálých datových formátů. Jakmile skončíte s budováním systému, změřte jeho výkonnost. Je-li dostatečně výkonný, pak jste hotovi. Pokud ne, vyhledejte zdroj problémů pomocí profilovacího nástroje a snažte se optimalizovat příslušné části systému. Prvním krokem je kontrola zvoleného algoritmu: Žádná optimalizace na nízké úrovni vám nepomůže kompenzovat špatně zvolený algoritmus. Opakujte tento proces podle potřeby a měřte výkonnost po každé změně, dokud nejste spokojeni.