Obsah
1. Programovací jazyk Clojure 18: základní techniky optimalizace aplikací
2. Referenčně transparentní funkce a jejich význam při optimalizaci aplikací
3. Využití funkce memoize pro úschovu a další použití již vypočtených návratových hodnot funkcí
4. Praktický příklad použití funkce memoize
5. Volba vhodných kolekcí při implementaci složitějších datových typů
7. Implementace seznamů v programovacím jazyku Clojure
8. Implementace vektorů v programovacím jazyku Clojure
1. Programovací jazyk Clojure 18: základní techniky optimalizace aplikací
„First make it right. Then make it fast“
V předchozích částech seriálu o Javě i o virtuálním stroji Javy (JVM) jsme se při popisu programovacího jazyka Clojure zabývali především tím, jak se v tomto jazyku používají jednotlivé technologie, které Clojure vývojářům nabízí. Popsali jsme si především způsob práce se symboly a hodnotami navázanými na tyto symboly, vytváření nových funkcí včetně funkcí anonymních, použití neměnných (immutable) kolekcí, použití sekvencí a samozřejmě taktéž lazy sekvencí, způsoby spouštění funkcí v samostatných vláknech, použití transakcí ve vícevláknových programech, navázání symbolů na takzvané validátory, kooperaci mezi programovacími jazyky Clojure a Java a v neposlední řadě jsme se taktéž zabývali makrosystémem programovacího jazyka Clojure. Ovšem prozatím jsme si řekli jen velmi málo informací o tom, jak je možné aplikace vytvářené v Clojure optimalizovat, a to jak s ohledem na rychlost výsledných aplikací (popř.&nbsb;na dobu odezvy) i s ohledem na paměťovou náročnost.
To, že se touto poměrně důležitou záležitostí zabýváme až v osmnácté části našeho povídání o programovacím jazyce Clojure je ve skutečnosti záměr, protože některé optimalizace je (především u velkých aplikací) možné a vhodné provádět až ve chvíli, kdy je aplikace z velké části funkční a kdy se na reálných datech ukáže, kde jsou ve skutečnosti největší problémy způsobující například pomalý běh aplikace, její pomalé odezvy či její velkou paměťovou náročnost. Čistě teoreticky by mělo být možné tato místa objevit již v době analýzy, ovšem poměrně velká část aplikací prochází redesignem až v době vývoje (zákazník například potřebuje do aplikace přidat další funkce, které změní strukturu databáze, což se v lepším případě projeví jen ve změně hierarchie tříd/funkcí, v horším případě pak v nutnosti vertikálně či horizontálně rozdělit databázi atd. atd.). Pokud se však větší část aplikace vytváří v programovacím jazyku Clojure, je možné se zpočátku soustředit na implementaci vlastních algoritmů, zaměřit se na správné použití funkcionálních technik (tím si programátor otevírá cestu k možnosti relativně snadno zvýšit výkonnost aplikace) a teprve posléze se zabývat optimalizací na nižších úrovních.
2. Referenčně transparentní funkce a jejich význam při optimalizaci aplikací
Již v úvodních článcích o programovacím jazyce Clojure jsme si řekli, že tento jazyk patří, společně s klasickým LISPem, Scheme, Haskellem či Erlangem do skupiny (ne vždy nutně čistě) funkcionálních jazyků, tj. programovacích jazyků vycházejících z teorie takzvaného λ-kalkulu, jehož autorem je Alonzo Church (na první návrhy LISPu se dokonce můžeme dívat jako na jeden z formalizovaných způsobů zápisu λ-kalkulu, pro nějž jen tak mimochodem existuje mechanismus vyhodnocování jednotlivých λ výrazů; taktéž se tím vysvětluje přítomnost znaku lambda v logu jazyka Clojure). Ve skutečnosti sice Clojure není čistě funkcionálním jazykem, ovšem v případě, že vývojář bude při tvorbě svých aplikací dodržovat zásady funkcionálního programování, bude pro něj mnohem snadnější vytvářet skutečně výkonné aplikace; ať se to již týká snadnější tvorby bezpečných vícevláknových aplikací či možnosti použití funkce memoize popsané v následující kapitole.
Připomeňme si, že v programovacím jazyce Clojure jsou funkce považovány za plnohodnotné datové typy, což znamená, že funkce lze navázat na libovolný symbol (a tím vlastně původně anonymní funkci pojmenovat), funkce lze předávat jako parametry do jiných funkcí a funkce mohou být taktéž návratovou hodnotou jiných funkcí – funkce tedy může vytvořit a vrátit jinou funkci. Clojure taktéž podporuje práci s uzávěry (closure(s)), tj. funkcí svázaných s nějakým symbolem vytvořeným vně funkce. Podpora uzávěrů umožňuje například tvorbu funkcí sdílejících společný kontext (GUI) atd. Ovšem vzhledem k tomu, že – jak již víme – Clojure není čistě funkcionálním jazykem, je možné při vytváření uživatelských funkcí přímo z dané funkce přistupovat k nějakému globálnímu symbolu, přesněji řečeno k symbolu „globálnímu“ v rámci nějakého jmenného prostoru. Taktéž lze vytvářet funkce s vedlejším efektem, které například zapisují data do souborů, mění hodnotu navázanou na globální symboly atd.
Vývojáři by však neměli tyto možnosti nabízené programovacím jazykem Clojure zneužívat, protože tím znemožňují využití některých optimalizačních technik a v neposlední řadě si taktéž komplikují možnost testování takto vytvořených funkcí. Namísto toho se ukazuje být velmi výhodné vytvářet takzvané referenčně transparentní funkce, což jsou funkce, které nepřistupují k žádným globálním symbolům, nemají žádný vedlejší efekt ani si nepamatují žádný vnitřní stav (příkladem „funkce“ s vnitřním stavem je například Math/random). Referenčně transparentní funkci jsou při jejím volání předány parametry a funkce pouze na základě hodnot předaných parametrů vrátí nějaký výsledek. Tato (pochopitelná) vlastnost má jeden důležitý důsledek – chování referenčně transparentní funkce je nezávislé na stavu aplikace a je taktéž zcela nezávislé na tom, kdy je funkce zavolána.
3. Využití funkce memoize pro úschovu a další použití již vypočtených návratových hodnot funkcí
Chování referenčně transparentní funkce je v čase neměnné a navíc platí, že pokud se funkce volá se stejnými parametry, vždy pro tyto parametry vrátí stejnou návratovou hodnotu. Díky tomu, že chování funkce je izolované od zbytku aplikace, nemusí se při jejím volání používat žádná synchronizace s ostatními vlákny (není ostatně bez zajímavosti, že výše zmíněná „funkce“ Math/random interně používá synchronizaci – zámky). V předchozí kapitole jsme si řekli, že návratová hodnota referenčně transparentní funkce je závislá pouze na hodnotě parametrů předaných této funkci, což mj. znamená, že je možné volání funkce nahradit konstantou, která byla někdy v minulosti volanou funkcí vrácena pro zadané parametry. Teoreticky je tedy možné vytvořit tabulku (či spíše mapu), která pro zadané parametry vrátí stejnou hodnotu, jaká by se vrátila při volání zvolené funkce. Jak je však možné tuto teoretickou vlastnost použít v praxi? Ve skutečnosti to není nijak obtížné, protože můžeme využít funkci memoize, která je součástí standardní knihovny programovacího jazyka Clojure.
Zajímavé je, že funkce memoize je implementována velmi jednoduše, což jen ukazuje, jak flexibilní programovací jazyk Clojure může být:
(defn memoize "Returns a memoized version of a referentially transparent function. The memoized version of the function keeps a cache of the mapping from arguments to results and, when calls with the same arguments are repeated often, has higher performance at the expense of higher memory use." {:added "1.0" :static true} [f] (let [mem (atom {})] (fn [& args] (if-let [e (find @mem args)] (val e) (let [ret (apply f args)] (swap! mem assoc args ret) ret)))))
Ve funkci memoize, které se v parametru f předává libovolná referenčně transparentní funkce (ve skutečnosti zcela libovolná funkce – žádná kontrola se neprovádí!), je interně použita mapa navázaná na symbol mem, do níž se jako klíče ukládají parametry volané funkce a jako příslušné hodnoty pak návratové hodnoty zavolané funkce. Dvojice klíč-hodnota mapy mem se postupně rozšiřují a vytváří se tak cache pro danou funkci. V případě, že se při „volání“ funkce f v mapě nalezne klíč odpovídající předávaným parametrům, vrátí se přímo hodnota uložená v mapě pomocí (val e), v opačném případě se funkce zavolá (forma apply f args) a její návratová hodnota se uloží do mapy mem (swap! …). Použití memoize má pouze tři nedostatky: interní mapu mem není možné při běhu aplikace smazat, neprovádí se žádné kontroly na to, zda je předávaná funkce skutečně referenčně transparentní a mapa s uloženými výsledky není automaticky serializována/deserializována při restartu aplikace – proto se při restartu začne vytvářet znovu (serializovanou mapu by navíc bylo možné použít v testech).
Povšimněte si, co je výsledkem volání funkce memoize – je jím anonymní funkce (přesněji řečeno uzávěr), kterou lze přiřadit k symbolu pomocí def/defn.
4. Praktický příklad použití funkce memoize
Ukažme si nyní, jak je možné v praxi využít funkci memoize popsanou v předchozí kapitole. V jedné z předcházejících částí tohoto seriálu byla jako demonstrační příklad použita rychlá varianta funkce pro výpočet faktoriálu. O rychlou variantu se jednalo z toho důvodu, že v ní nebyla použita rekurze (přesněji řečeno tail rekurze), ale namísto toho se jednoduše aplikovala funkce * (násobení) na sekvenci hodnot 1..n. Mírně upravená rychlá podoba funkce fact je vypsána pod tímto odstavcem. Úprava oproti tvaru funkce použité v předchozích článcích spočívá v použití konstanty 1M při volání funkce range, čímž docílíme toho, že se při vlastním výpočtu namísto hodnot typu long (rozsah 64 bitů) budou používat instance třídy java.math.BigDecimal, čímž se rozšíří rozsah výsledných hodnot:
; rychlý výpočet faktoriálu (defn fact [n] (apply * (range 1M (inc n))))
Výpočet faktoriálu je sice skutečně relativně rychlý, ovšem v případě, že budeme tuto funkci volat stále se stejným parametrem, bude se výpočet neustále opakovat. To se již může negativně projevit na výsledném výkonu aplikace, zejména tehdy, pokud se bude počítat faktoriál velkého čísla n, což v dané implementaci vyžaduje n násobení, které není v případě java.math.BigDecimal rychlou operací. Ostatně se o tom můžeme jednoduše přesvědčit sami:
user=> (dotimes [i 10] (time (fact 10000))) "Elapsed time: 1351.005784 msecs" "Elapsed time: 1308.91524 msecs" "Elapsed time: 1307.073668 msecs" "Elapsed time: 1367.755396 msecs" "Elapsed time: 1516.798644 msecs" "Elapsed time: 1357.112148 msecs" "Elapsed time: 1343.379676 msecs" "Elapsed time: 1397.393524 msecs" "Elapsed time: 1319.43194 msecs" "Elapsed time: 1311.928468 msecs" nil user=>
Každý výpočet trval přibližně 1,3 sekundy, nezávisle na tom, že jsme volali referenčně transparentní funkci se stále stejným parametrem. Pokud však využijeme funkci memoize, bude vše vypadat zcela jinak:
; rychlý výpočet faktoriálu se zapamatováním výsledku výpočtu (def fact2 (memoize ( fn [n] (apply * (range 1M (inc n))))))
Předchozí zápis znamená vytvoření nového symbolu se jménem fact2 navázaného na výsledek funkce memoize, které se předala anonymní funkce pro výpočet faktoriálu (vlastní zápis výpočtu se vůbec nezměnil). Při každém volání fact2 se zkontroluje, zda již pro předávaný parametr neexistuje výsledek a ten se případně použije. Časy běhu funkce fact2 pro stejný parametr pak budou diametrálně odlišné:
user=> (dotimes [i 10] (time (fact2 10000))) "Elapsed time: 1320.770096 msecs" "Elapsed time: 0.022908 msecs" "Elapsed time: 0.013688 msecs" "Elapsed time: 0.013692 msecs" "Elapsed time: 0.013688 msecs" "Elapsed time: 0.013408 msecs" "Elapsed time: 0.013412 msecs" "Elapsed time: 0.013412 msecs" "Elapsed time: 0.013132 msecs" "Elapsed time: 0.013968 msecs" nil user=>
Vidíme, že výpočet proběhl pouze jednou a posléze se již prakticky ihned vracel výsledek uložený ve vyrovnávací paměti (mapě)! Jedná se tedy o klasický typ optimalizace, v níž se programátor rozhoduje mezi dobou běhu a nároky na operační paměť.
5. Volba vhodných kolekcí při implementaci složitějších datových typů
Kromě snahy o použití referenčně transparentních funkcí může programátor dosti podstatným způsobem ovlivnit výkonnost aplikace taktéž volbou vhodných typů kolekcí, s jejichž využitím se vytváří složitější datové struktury – stromy, grafy atd. Připomeňme si, že programovací jazyk Clojure podporuje čtyři typy kolekcí. Ve všech případech se jedná o neměnitelné (immutable) kolekce, jejichž obsah není možné měnit – nelze do nich tedy přidávat další prvky, ubírat prvky ani měnit hodnotu prvků. Ve standardní knihovně ovšem existuje velké množství funkcí, které vezmou kolekci jako svůj vstupní parametr a vrátí kolekci jinou, například původní kolekci s přidaným novým prvkem atd. (původní kolekce však zůstane nezměněna). Všechny čtyři typy kolekcí jsou i se svými konstruktory vypsány v následující tabulce:
Typ kolekce | Zápis kolekce (syntaktický cukr) | Konstruktor |
---|---|---|
Seznam | (prvky) | (prvky) |
Vektor | [prvky] | (vector prvky) |
Mapa | {dvojice klíč-hodnota} | (hash-map dvojice klíč-hodnota) |
Množina | #{unikátní prvky} | (hash-set unikátní prvky) |
Při volbě vhodné kolekce, která má tvořit základ datové struktury, se vývojář může orientovat i podle toho, jaké rozhraní (ve smyslu programovacího jazyka Java) jednotlivé kolekce implementují. Tím jsou určeny základní vlastnosti každé kolekce:
Rozhraní | seznam | vektor | mapa | množina |
---|---|---|---|---|
java.util.Collection | ano | ano | ne | ano |
java.util.List | ano | ano | ne | ne |
java.util.Map | ne | ne | ano | ne |
java.util.Set | ne | ne | ne | ano |
java.util.RandomAccess | ne | ano | ne | ne |
java.lang.Iterable | ano | ano | ano | ano |
java.lang.Comparable | ne | ano | ne | ne |
Programátoři většinou nemívají problém s rozhodnutím, zda se má použít seznam/vektor na jedné straně či mapa na straně druhé – pokud se má při přístupu k prvkům použít neceločíselný klíč, je většinou na místě použít mapu. Ovšem poněkud horší situaci má vývojář při rozhodování, pro které datové struktury má využít seznam (list) a kde naopak vektor (vector). To je i téma následující kapitoly.
Odkazy na související články:
- Clojure Data Structures
http://clojure.org/data_structures - The Structure and Interpretation of Computer Programs: 2.2.1 Representing Sequences
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html#%_sec2.2.1 - The Structure and Interpretation of Computer Programs: 3.3.1 Mutable List Structure
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-22.html#%_sec3.3.1
6. Seznamy vs. vektory
Programovací jazyk LISP, z něhož je Clojure ve velké míře odvozen, byl původně založen prakticky pouze na použití seznamů, které se využívaly jak pro ukládání dat, tak i pro reprezentaci vlastního programu, protože program (jeho funkce) lze reprezentovat jako seznam seznamů. Nejinak je tomu i v programovacím jazyce Clojure, jenž však kromě seznamů poměrně intenzivně používá i vektory (například parametry funkce se zapisují do vektorů atd.), což musí mít svoje opodstatnění. Seznamy a vektory se totiž od sebe odlišují především svojí vnitřní strukturou, z níž přímo vyplývají i operace, které lze s těmito kolekcemi provádět a v neposlední řadě i složitost jednotlivých operací, která nás dnes taktéž zajímá.
Kolekce | Implementované rozhraní | Implementováno v… | Predikát |
---|---|---|---|
Seznam | IPersistentList | PersistentList.java | list? |
Vektor | IPersistentVector | PersistentVector.java | vector? |
Jeden z rozdílů mezi oběma kolekcemi spočívá v tom, že prvky vektorů je možné číst jako přímou sekvenci i jako sekvenci otočenou (od posledního prvku k prvku prvnímu). Nad vektorem lze tedy zavolat funkci seq i rseq, zatímco u seznamů pouze funkci seq – prvky lze číst pouze od začátku seznamu do konce. Další rozdíl mezi seznamy a vektory spočívá v odlišném chování funkce conj, kterou lze použít k vytvoření kopie původní kolekce doplněné o nové prvky. U seznamů se prvky přidávají na jeho začátek, zatímco u vektorů je tomu přesně naopak – vrácen je nový vektor s prvky přidanými na jeho konci. Podobně se liší chování funkcí pop a peek. Shrňme si nyní jednotlivé funkční rozdíly mezi seznamy a vektory přehledně do tabulky:
# | Funkce | Seznam | Vektor |
---|---|---|---|
1 | seq | podporováno | podporováno |
2 | rseq | není podporováno | podporováno |
3 | conj | nové prvky + původní seznam | vektor + nové prvky |
4 | pop | seznam bez prvního prvku | vektor bez posledního prvku |
5 | peek | první prvek seznamu | poslední prvek vektoru |
5 | first | první prvek seznamu | první prvek vektoru |
6 | last | poslední prvek seznamu | poslední prvek vektoru |
7 | nth | n-tý prvek seznamu | n-tý prvek vektoru |
8 | count | počet prvků seznamu | počet prvků vektoru |
Důležité je při optimalizaci aplikací i porovnání výpočetní složitosti některých funkcí z předchozí tabulky, protože právě odlišná složitost může výrazným způsobem ovlivnit výkonnost celé aplikace, zejména tehdy, pokud se budou používat kolekce s velkým množstvím prvků. Zajímavé je, že funkce count má v obou případech konstantní složitost: O(1). To znamená, že v Clojure nemá smysl počítat počet prvků seznamu s využitím rekurzivní funkce, což je jeden z oblíbených školních příkladů používaných při výuce LISPu :-) Naproti tomu funkce nth se sice u obou typů kolekcí chová stejně, má však výrazně odlišnou složitost: O(n) v případě seznamů (lineární složitost) a O(log32N) v případě vektorů (logaritmická složitost). U krátkých vektorů (do 32 prvků) je složitost konstantní a i u delších vektorů je počet kroků nutných pro získání n-tého prvku velmi malý (například dva kroky pro vektory o délce přibližně 1000 prvků atd.). Složitost funkce peek je v případě vektoru taktéž rovna O(log32N), na rozdíl od funkce last se složitostí O(N) – v případě vektorů tedy vždy používejte peek!
# | Funkce | Seznam | Vektor |
---|---|---|---|
1 | count | O(1) | O(1) |
2 | nth | O(N) | O(log32N) |
3 | pop | O(1) | O(log32N) |
4 | peek | O(1) | O(log32N) |
5 | first | O(1) | O(1) |
6 | last | O(N) | O(N) |
Povšimněte si, že vektory ve skutečnosti neodpovídají složitostí některých funkcí běžně chápaným vektorům-jednorozměrným polím. Je tomu tak z toho důvodu, že v Clojure jsou vektory implementovány odlišným způsobem a to zejména proto, aby bylo možné jednoduše implementovat funkci conj, tj. aby se již vytvořená datová struktura mohla sdílet mezi větším množstvím vektorů (to je možné i díky jejich neměnnosti), Proč se od sebe seznamy a vektory tak diametrálně odlišují, si řekneme v následujících dvou kapitolách.
7. Implementace seznamů v programovacím jazyku Clojure
Programovací jazyk LISP, který je ideovým předchůdcem jazyka Clojure, byl původně založen prakticky pouze na použití seznamů, které byly považovány za zcela univerzální datovou strukturu. V tradičních LISPech jsou seznamy implementované pomocí takzvaných tečka-dvojic (dot-pair), což jsou dvojice prvků, přičemž každý prvek buď může obsahovat nějakou hodnotu nebo obsahuje odkaz na další tečka-dvojici. Běžný seznam pak byl vytvořen jako jednosměrně vázaný lineární seznam složený z tečka-dvojic, kde první prvek ve dvojici obsahoval nějakou hodnotu a druhý prvek pak odkaz na prvek následující. U některých variant programovacího jazyka LISP se navíc obsah každého prvku v tečka dvojici rozděloval na takzvaný tag (řekněme zjednodušeně popis datového typu) a vlastní hodnotu, což bylo z hlediska obsazení operační paměti poměrně efektivní řešení. Pro přístup k prvkům seznamů se používaly dvě funkce pojmenované z historických důvodů car a cdr. Funkce car vracela první prvek seznamu a funkce cdr zbytek seznamu, tj. všechny prvky následující.
Vzhledem k tomu, že seznam byl implementován formou na sebe navazujících tečka dvojic, byla implementace obou funkcí car/cdr triviální a tyto funkce měly konstantní složitost. Naproti tomu funkce nth (přístup k n-tému prvku seznamu) měla složitost lineární: O(n). Podobně funkce zjišťující počet prvků seznamu count měla lineární složitost. V případě programovacího jazyka Clojure je situace poněkud odlišná od většiny dalších implementací LISPu. První odlišnost spočívá v tom, že prvky seznamu jsou typu java.lang.Object a jedná se tedy vždy o reference a nikoli přímo o hodnoty. Na celý seznam se tedy můžeme dívat jako na posloupnost referencí, interně je ovšem celý seznam tvořen rekurzivní strukturou složenou z prvního prvku a zbytku seznamu (jde tedy o obdobu tečka-dvojic) – právě tato vlastnost vede k tomu, že přidání dalších prvků na začátek seznamu pomocí funkce conj je výpočetně nenáročnou operací. Vlastní hodnoty jsou pak uloženy někde na haldě (heapu). Druhá odlišnost spočívá v tom, že každý seznam si pamatuje svoji délku, takže složitost funkce count je v jazyce Clojure konstantní.
Jen pro zajímavost se podívejme na část kódu s implementací seznamu:
public class PersistentList extends ASeq implements IPersistentList, IReduce, List, Counted { // první prvek seznamu private final Object _first; // další prvky seznamu private final IPersistentList _rest; // délka seznamu private final int _count; // složitost O(1) public Object first(){ return _first; } // složitost O(1) public ISeq next(){ if(_count == 1) return null; return (ISeq) _rest; } // složitost O(n) public Object get(int index){ return RT.nth(this, index); }
Volání funkce nth interně vede k volání následující metody:
static Object nthFrom(Object coll, int n){ else if(coll instanceof Sequential) { ISeq seq = RT.seq(coll); coll = null; for(int i = 0; i <= n && seq != null; ++i, seq = seq.next()) { if(i == n) return seq.first(); } throw new IndexOutOfBoundsException();
8. Implementace vektorů v programovacím jazyku Clojure
V programovacím jazyku Clojure se na mnoha místech seznamy nahrazují za vektory, protože práce s vektory může být více efektivní. Vektor můžeme považovat za jednorozměrné pole hodnot libovolných typů, takže by se mohlo zdát, že složitost přístupu k jeho prvkům bude konstantní O(N). Ve skutečnosti jsou však vektory interně implementovány poněkud složitějším (a velmi zajímavým) způsobem a to především z toho důvodu, aby bylo snadné k vektorům připojovat další prvky – tak vznikne nový vektor, ovšem původní vektor musí samozřejmě zůstat zachovaný. Proto se v Clojure (a v některých dalších moderních programovacích jazycích) používají pro implementaci vektorů takzvané RRB-Stromy (RRB-Trees, Relaxed Radix Balanced Trees). V Clojure jsou použity (vyvážené) RRB-stromy, které mají v každém listu uloženo jednorozměrné pole o délce 32 prvků. Pokud se pracuje s kratším vektorem, je pro jeho uložení použit strom pouze s jedním listem a tudíž je vektor skutečně reprezentován jednorozměrným polem (ve skutečnosti se vedle vlastního stromu používá ještě pomocné pole tail, pro jednoduchost však jeho roli v tomto textu poněkud zanedbáme).
U delších vektorů se v 32prvkovém poli uloženém v kořenu stromu nenachází přímo prvky vektoru, ale reference na další listy, z nichž každý opět obsahuje 32prvkové pole. To znamená, že vektor s až 1024 prvky může být uložen ve stromu o výšce 1, ve stromu o výšce 2 je celkový (možný) počet prvků vektoru roven 32768 (1024 listů á 32 prvků) atd. Operace vrácení n-tého prvku má tedy složitost O(log32N), což sice není konstantní složitost O(1), ale pro vektory běžných délek můžeme tuto složitost taktéž považovat za konstantní. Předností vektorů je tedy rychlejší přístup k jeho prvkům, ale nesmíme zapomenout ani na paměťové nároky – vektory interně potřebují mnohem méně referencí, než je tomu u seznamů, tudíž i paměťové nároky budou nižší, a to zejména na 64bitových systémech; samozřejmě pokud zde uživatelé nepovolí použití komprimovaných ukazatelů (compressed oops).
Interní strukturu vektoru opět můžeme zjistit přímo ze zdrojových textů programovacího jazyka Clojure:
public class PersistentVector extends APersistentVector implements IObj, IEditableCollection{ // délka vektoru final int cnt; // výška stromu final int shift; // kořen stromu final Node root; // až 32 posledních prvků vektoru je uloženo v tomto pomocném poli final Object[] tail;
Zajímavá je implementace funkce nth:
// získání i-tého prvku vektoru public Object nth(int i){ // uzel stromu, který obsahuje příslušný prvek Object[] node = arrayFor(i); // nyní nás zajímá pouze index 0..31 return node[i & 0x01f]; } // rekurzivní vyhledání příslušného listu public Object[] arrayFor(int i){ if(i >= 0 && i < cnt) { // prvek hledejme v pomocném poli tail if(i >= tailoff()) return tail; Node node = root; // vlastní rekurzivní průchod stromem for(int level = shift; level > 0; level -= 5) node = (Node) node.array[(i >>> level) & 0x01f]; return node.array; } throw new IndexOutOfBoundsException(); } final int tailoff(){ if(cnt < 32) return 0; return ((cnt - 1) >>> 5) << 5; }
9. Odkazy na Internetu
- SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html - Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html - Clojure Macro Tutorial (Part III: Syntax Quote)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html - Tech behind Tech: Clojure Macros Simplified
http://techbehindtech.com/2010/09/28/clojure-macros-simplified/ - Fatvat – Exploring functional programming: Clojure Macros
http://www.fatvat.co.uk/2009/02/clojure-macros.html - Eulerovo číslo
http://cs.wikipedia.org/wiki/Eulerovo_číslo - List comprehension
http://en.wikipedia.org/wiki/List_comprehension - List Comprehensions in Clojure
http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html - Clojure Programming Concepts: List Comprehension
http://en.wikibooks.org/wiki/Clojure_Programming/Concepts#List_Comprehension - Clojure core API: for macro
http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/for - cirrus machina – The Clojure for macro
http://www.cirrusmachina.com/blog/comment/the-clojure-for-macro/ - Clojure.org: Clojure home page
http://clojure.org/downloads - Clojure.org: Vars and the Global Environment
http://clojure.org/Vars - Clojure.org: Refs and Transactions
http://clojure.org/Refs - Clojure.org: Atoms
http://clojure.org/Atoms - Clojure.org: Agents as Asynchronous Actions
http://clojure.org/agents - A Couple of Clojure Agent Examples
http://lethain.com/a-couple-of-clojure-agent-examples/ - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 4Clojure
http://www.4clojure.com/ - ClojureDoc
http://clojuredocs.org/ - Clojure (Wikipedia EN)
http://en.wikipedia.org/wiki/Clojure - Clojure (Wikipedia CS)
http://cs.wikipedia.org/wiki/Clojure - Riastradh's Lisp Style Rules
http://mumble.net/~campbell/scheme/style.txt - Dynamic Languages Strike Back
http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html - Scripting: Higher Level Programming for the 21st Century
http://www.tcl.tk/doc/scripting.html - Java Virtual Machine Support for Non-Java Languages
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html - New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
http://java.sun.com/developer/technicalArticles/DynTypeLang/ - JSR 223: Scripting for the JavaTM Platform
http://jcp.org/en/jsr/detail?id=223 - JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform
http://jcp.org/en/jsr/detail?id=292 - Java 7: A complete invokedynamic example
http://niklasschlimm.blogspot.com/2012/02/java-7-complete-invokedynamic-example.html - InvokeDynamic: Actually Useful?
http://blog.headius.com/2007/01/invokedynamic-actually-useful.html - A First Taste of InvokeDynamic
http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html - Java 6 try/finally compilation without jsr/ret
http://cliffhacks.blogspot.com/2008/02/java-6-tryfinally-compilation-without.html - An empirical study of Java bytecode programs
http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/ - Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
http://www.mobilefish.com/tutorials/java/java_quickguide_jvm_instruction_set.html - The JVM Instruction Set
http://mpdeboer.home.xs4all.nl/scriptie/node14.html - Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.html - Root.cz: Využití komprimovaných ukazatelů na objekty v JVM
http://www.root.cz/clanky/vyuziti-komprimovanych-ukazatelu-na-objekty-v-nbsp-jvm/ - Root.cz: JamVM aneb alternativa k HotSpotu nejenom pro embedded zařízení a chytré telefony
http://www.root.cz/clanky/jamvm-aneb-alternativa-k-hotspotu-nejenom-pro-embedded-zarizeni-tablety-a-chytre-telefony/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - BCEL Home page
http://commons.apache.org/bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - FindBugs
http://findbugs.sourceforge.net/ - GNU Classpath
www.gnu.org/s/classpath/ - Java VMs Compared
http://bugblogger.com/java-vms-compared-160/ - JSRs: Java Specification Requests – JSR 223: Scripting for the Java Platform
http://www.jcp.org/en/jsr/detail?id=223 - Scripting for the Java Platform
http://java.sun.com/developer/technicalArticles/J2SE/Desktop/scripting/ - Scripting for the Java Platform (Wikipedia)
http://en.wikipedia.org/wiki/Scripting_for_the_Java_Platform - Java Community Process
http://en.wikipedia.org/wiki/Java_Specification_Request - Java HotSpot VM Options
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html - Great Computer Language Shootout
http://c2.com/cgi/wiki?GreatComputerLanguageShootout - Java performance
http://en.wikipedia.org/wiki/Java_performance - Trying the prototype
http://mail.openjdk.java.net/pipermail/lambda-dev/2010-August/002179.html - Better closures (for Java)
http://blogs.sun.com/jrose/entry/better_closures - Lambdas in Java: An In-Depth Analysis
http://www.infoq.com/articles/lambdas-java-analysis - Class ReflectiveOperationException
http://download.java.net/jdk7/docs/api/java/lang/ReflectiveOperationException.html - Scala Programming Language
http://www.scala-lang.org/ - Run Scala in Apache Tomcat in 10 minutes
http://www.softwaresecretweapons.com/jspwiki/run-scala-in-apache-tomcat-in-10-minutes - Fast Web Development With Scala
http://chasethedevil.blogspot.cz/2007/09/fast-web-development-with-scala.html - Top five scripting languages on the JVM
http://www.infoworld.com/d/developer-world/top-five-scripting-languages-the-jvm-855 - Proposal: Indexing access syntax for Lists and Maps
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/001108.html - Proposal: Elvis and Other Null-Safe Operators
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/000047.html - Java 7 : Oracle pushes a first version of closures
http://www.baptiste-wicht.com/2010/05/oracle-pushes-a-first-version-of-closures/ - Groovy: An agile dynamic language for the Java Platform
http://groovy.codehaus.org/Operators - Better Strategies for Null Handling in Java
http://www.slideshare.net/Stephan.Schmidt/better-strategies-for-null-handling-in-java - Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.html - Java Virtual Machine
http://en.wikipedia.org/wiki/Java_virtual_machine - ==, .equals(), compareTo(), and compare()
http://leepoint.net/notes-java/data/expressions/22compareobjects.html - New JDK7 features
http://openjdk.java.net/projects/jdk7/features/ - Project Coin: Bringing it to a Close(able)
http://blogs.sun.com/darcy/entry/project_coin_bring_close - CloseableFinder source code
http://blogs.sun.com/darcy/resource/ProjectCoin/CloseableFinder.java - Joe Darcy blog about JDK
http://blogs.sun.com/darcy - Java 7 – more dynamics
http://www.baptiste-wicht.com/2010/04/java-7-more-dynamics/ - New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
http://java.sun.com/developer/technicalArticles/DynTypeLang/index.html