400 likes | 597 Views
Optimalizace algoritmů. Jiří „Fila“ Filipovič PUPworX. Předmět přednášky. časová složitost algoritmu význam často používané metody snižování složitosti lineární optimalizace kdy a kde optimalizovat základní metody, pro pokročilé asi nic nového…. Složitost algoritmu.
E N D
Optimalizace algoritmů Jiří „Fila“ Filipovič PUPworX
Předmět přednášky • časová složitost algoritmu • význam • často používané metody snižování složitosti • lineární optimalizace • kdy a kde optimalizovat • základní metody, pro pokročilé asi nic nového…
Složitost algoritmu • teorie složitosti je velmi komplexní, bude demonstrován jen velmi zúžený pohled • složitostní třída popisuje chování algoritmu vzhledem k délce vstupu • časová vs. prostorová složitost • nejlepší, nejhorší a průměrný případ • zajímá nás asymptotické chování funkce složitosti
Vlastnosti složitostních tříd • abstrahujeme od konkrétního času běhu algoritmu • nezávislost na CPU, kompilátoru… • O(g) – třída funkcí rostoucích nejvýše tak rychle jako g • Zajímá nás asymptotické chování • T(log2 n) = O(log n) • T(3n + 1) = O(n) • T(n2 - n + 3) = O(n2)
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; }
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; } • jaká je časová složitost?
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; } • jaká je časová složitost? • nejlepší případ O(1)
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; } • jaká je časová složitost? • nejlepší případ O(1) • nejhorší případ O(n)
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; } • jaká je časová složitost? • nejlepší případ O(1) • nejhorší případ O(n) • průměr O(n/2) – za jakých předpokladů?
Určení složitosti - příklad • vyhledání prvku v poli int hledejSekvencne(int pole[n], int prvek){ for (int i = 0; i < n; i++) if (pole[i] == prvek) return i; return -1; } • jaká je časová složitost? • nejlepší případ O(1) • nejhorší případ O(n) • průměr O(n/2) – za jakých předpokladů? • jde to rychleji?
Určení složitosti - příklad int hledejBinarne(int pole[n], int prvek){ int leva = 0; int prava = n; while (leva <= prava){ int stred = (prava-leva)/2 + leva; if (prvek > pole[stred]) leva = stred + 1; else if (prvek < pole[stred]) prava = stred - 1; else return stred; } return -1; }
Určení složitosti - příklad int hledejBinarne(int pole[n], int prvek){ int leva = 0; int prava = n; while (leva <= prava){ int stred = (prava-leva)/2 + leva; if (prvek > pole[stred]) leva = stred + 1; else if (prvek < pole[stred]) prava = stred - 1; else return stred; } return -1; } • časová složitost O(log n)
Snížení čas. složitosti • některé metody jsou genericky využitelné v mnoha problémech: • předpočítávání • stromy • hashe • heuristiky • aproximace
Předpočítávání • použitelné pokud se pohybujeme v relativně malé množině hodnot, které je složité získat • typický případ snížení časové složitosti na úkor složitosti prostorové • časová složitost O(1) • prostorová je úměrná velikosti předpočítaných dat • pozor – cache miss je řádově 100x pomalejší než běžná instrukce – něco je dnes snazší počítat, než načítat z RAM
Předpočítávání - příklady • goniometrické funkce • Pro nižší přesnost (předpočítá se omezený počet úhlů) • dnes je levnější aproximace Taylorovým rozvojem • vesmírný simulátor • realistické gravitační pole • předpočítávání viditelnosti • PVS • bourání domů • složitá fyzika počítána v čase t+n
Stromy • běžné užití • vyhledávání prvku (O(log n)) • setřídění (O(n log n)) • při modifikaci problém vyvažování • nevyvážený strom ztrácí požadované vlastnosti • užití: • setřídění pole čísel, nalezení čísla • vytvoření prostorové hierarchie • vytvoření obálkové hierarchie
Hashe • vyhledávání v relativně malé množině prvků z velké domény • slovníky • prostorové umístění těles • hesla • aj. • cíl – obvykle redukovat složitost vyhledávání na O(1)
Hashovací funkce • mapuje vstupní klíč do indexu tabulky • hodnoty musí být mapovány rovnoměrně a náhodně • pokud má více klíčů stejný hash, prohledávají se tyto hashe sekvenčně (nebo je třeba je setřídit) • nalezení dobré hashovací funkce je někdy složité až nemožné (stojíme před úkolem zajistit náhodné mapování nenáhodných dat) • hashovací funkci často hledáme pokusem a omylem, některé operace se obecně hodí, využíváme velkých prvočísel
Příklad – hashování prostoru • metoda pro první fázi detekce kolizí • prostor je rozdělen regulární mřížkou, podle polohy na mřížce lze zakódovat umístění těles • potenciálně kolidují tělesa se objeví na stejných indexech tabulky • výhodné při velkém množství pohybujících-se objektů • blíží se O(n) • zpomaluje nevhodná velikost mřížky, malá tabulka
Příklad hashování prostoru • Červený objekt se zahashuje do 0000 • Modrý do 0010 a 0110 • Žlutý do 0101 a 0110 • Zelený do 0100, 0101, 1000 a 1001 • Jsou detekovány potenciální kolize: • Modrý x žlutý • Žlutý x zelený
Heuristiky • „ořezávání“ nezajímavých větví výpočtu • problém s nalezením vhodných kritérií • kterou větev zahodit, když nevidíme budoucnost? • užití např. • u pathfindingu (zahazování drahých cest) • v deskových hrách (zahazování nevýhodných tahů)
Aproximace • nalezení přibližného výsledku • někdy nutnost (fyzika), někdy výrazná časová úspora (LOD, předpočítávání spojitých funkcí, heuristiky) • problém obchodního cestujícího: • O(n!) • Pomocí GA – počítáme dokud chceme lepší výsledek
Lineární optimalizace • optimalizace „na úrovni programových řádků“ • zůstáváme v téže složitostní třídě, rychlost zvyšujeme pouze násobkem • je dobré znát náročnost běžných programových konstrukcí • je dobré znát hardware, na kterém program poběží • je dobré znát programové prostředí, náročnost knihovních funkcí atd.
Volání funkcí • volání funkcí představuje určitou režii • kompilátor umí sám rozhodnout, která funkce bude linkována inline • rychlost volání funkce závisí na počtu a velikosti(!) parametrů • velká data je vhodnější předávat odkazem • virtuální metody jsou pomalejší • Nepoužívejte pozdní vazbu u jednoduchých a často volaných metod
Výkonnost hardware • elementární aritmetické operace +, - a * jsou velmi rychlé, a to i v plovoucí řádové čárce • neceločíselné dělení je o řád pomalejší • goniometrické funkce jsou pomalejší než dělení • některé operace lze vektorizovat • paměť je škálována – registry, L2 cache, RAM, HDD… • někdy je lepší počítat, než „sahat“ do paměti
Odstranění rekurze • rekurze je drahá • (pře)plnění zásobníku • režie s voláním a předáváním parametrů • některé algoritmy jsou v iterační formě špatně čitelné • neexistuje obecný návod, jak rekurzi odstranit
Odstranění rekurze - příklad unsigned faktorial(unsigned x){ if (x == 0) return 1; else return x*faktorial(x-1); }
Odstranění rekurze - příklad unsigned faktorial(unsigned x){ if (x == 0) return 1; else return x*faktorial(x-1); } unsigned faktorialOpt(unsigned x, unsigned p = 1){ if (x == 0) return p; else return faktorialOpt(x-1, x*p); }
Kdy optimalizovat? • optimalita kódu je nepřímo úměrná jeho čitelnosti • nesoustřeďte se na kód na úrovni programových řádků, hledejte úzká hrdla • někdy je úzké hrdlo zjevné • někdy zjevné není • nepočítali jsme s tím, že se daný kód volá tak často • některá funkce trvá déle, než jsme si mysleli • přehlédli jsme zbytečné volání či nezoptimalizovanou funkci • je dobré používat profiler – najde chyby rychleji než my
Kdy neoptimalizovat? • často optimalizujeme ze zvyku i tam, kde to není nutné • existují třídící algoritmy v O(n2) a O(n log n), má smysl o tom uvažovat u setřídění listiny vítězů, kterých je max. 20? • pozná někdo co dělá toto? while(*s++ = *t++);
Kdy neoptimalizovat? • často optimalizujeme ze zvyku i tam, kde to není nutné • existují třídící algoritmy v O(n2) a O(n log n), má smysl o tom uvažovat u setřídění listiny vítězů, kterých je max. 20? • pozná někdo co dělá toto? while(*s++ = *t++); • nemachrujte – nikdy nevíte, kdo to po vás bude číst!
Kdy neoptimalizovat? • často optimalizujeme ze zvyku i tam, kde to není nutné • existují třídící algoritmy v O(n2) a O(n log n), má smysl o tom uvažovat u setřídění listiny vítězů, kterých je max. 20? • pozná někdo co dělá toto? while(*s++ = *t++); • nemachrujte – nikdy nevíte, kdo to po vás bude číst! • neoptimalizujte části kódu, které se volají současně s výrazně náročnější operací • šetření času CPU při načítání z disku (ovšem pokud nejedete multivláknově) • výpočty před/po dlouhým cyklem
Profiler • dynamická analýza výkonnosti programu • měří, kolik času tráví CPU v jednotlivých funkcích, jak často je volá • obvykle umožňuje přímo diassemblovat jednotlivé řádky kódu • dobře použitelný u zpracování dat s nízkou úrovní paralelizace • je dobré připravit profileru různé „scénáře“