Types will carry you over the Monads

Teď nechci zabřednout do bažin internetu a napsat další monádový tutoriál, ale jen zveřejnit jeden postřeh, díky kterému jsem pochopil sílu Applicatives:

Vždycky, když jsi v úzkých, následuj typy a ty tě dovedou do cíle.


Funkce bind definovaná na monádě má typ:

bind :: M a -> (a -> M b) -> M b

Ten říká, že mám nějaké M a a pak funkci z a do M b a když je nějak zkombinuji, dostanu M b. Z typu je vidět, že do funkce a -> M b musím dodat a, které je uvězněno v M a a jinak není možné pokračovat. Monády tedy popisují operace, které jsou sekvenční a jeden krok závisí na výsledku toho předchozího.

Pokud tedy například M a reprezentuje asynchronní operaci, která eventuálně vyprodukuje hodnotu typu a, argument a -> M b představuje další async akci, která k tomu, aby vůbec začala, potřebuje znát a – tedy předchozí výsledek. Jde tedy o řetězení async operací, které je jasně patrné z typu funkce.


Applicative definuje jednu funkci, které tady budu říkat apply 1 a která má typ:

apply :: M a -> M (a -> b) -> M b

Ten říká, že mám nějaké M a a nějaké M (a -> b) (tedy funkci a -> b uvnitř M) a když je nějak zkombinuji, dostanu M b. Ale na rozdíl od předchozího případu, M (a -> b) nepotřebuje a k tomu aby začal pracovat.

Pokud je M opět asynchronní operace, pak druhý argument představuje jinou asynchronní operaci, která nezávisle na prvním argumentu vyprodukuje funkci a -> b. Když jsou oba dva argumenty připraveny, je možné zavolat funkci z druhého argumentu s výsledkem toho prvního a dostat tak výsledek reprezentovaný async operací M b.

Opět: I tohle je patrné z typu funkce a není možné to udělat jinak a stále vyhovovat typové signatuře.


Pro úplnost ještě uvedu funktor, jehož funkce fmap má typ:

fmap :: M a -> (a -> b) -> M b

Když by M byla zase async operace, pak argument a -> b představuje pure transformaci výsledku této operace, který sám nedělá žádná async kouzla.

To všechno je zas patrné z pouhého typu.

Ještě se sluší dodat, že každá monáda je applicative a každá applicative je funktor.

Doufám, že jsem tedy nakonec nenapsal další monádový tutoriál, ale něco, co bude aspoň trochu užitečné, A mimochodem: Titulek je variace na jméno alba We will carry you over the mountains od Magyar Posse.


  1. V Haskellu tahle funkce má jiné pořadí argumentů a nese jméno (<*>), které může být podobně jako axaxaxas mlö vysloveno jen jako posměšný a krutý smích autorů Haskellu.

flattr this!

Posted in Funkcionální programování, Typy | Leave a comment

PHP DOM, SimpleXML a Matcher

Však to znáte: Tak dlouho chodíte se džbánem pro vodu, až si na to napíšete framework. Já jsem tak dlouho crawloval a robotoval, až jsem si na to napsal Matcher. I když k frameworku má velice daleko, protože jde jen o dvě třídy.

Pokud chci v PHP extrahovat data z HTML dokumentu, mám na výběr mezi dvěma zly: DOM a SimpleXML.

Kód, který používá DOM může vypadat nějak takto:

$dom = @ \DOMDocument::loadHTML($htmlString);
$xpath = new \DOMXpath($dom);
$nodes = $xpath->query('//div[@class="post"]');
$res = [];
foreach ($nodes as $node) {
  $res[] = [
    'id'    => $xpath->query('@id', $node)->item(0)->textContent,
    'date'  => $xpath->query('div[@class="date"]', $node)->item(0)->textContent,
    'title' => $xpath->query('h2', $node)->item(0)->textContent,
    'text'  => $xpath->query('div[@class="body"]', $node)->item(0)->textContent,
  ];
}

Jde o víc psaní než by se mi líbilo. Navíc s každou úrovní zanoření požadovaných dat přibude jedna vnitřní smyčka.

SimpleXML se může zdát jako lepší volba, protože má na první pohled přehlednější API, ale to je jenom past na nepozorné. Tak předně SimpleXML nedokáže načíst HTML dokument a je nutno ho nejdřívě naparsovat DOMem a teprve potom importovat do SimpleXML.

$dom = @ \DOMDocument::loadHTML($htmlString);
$xml = simplexml_import_dom($dom);
$nodes = $xml->xpath('//div[@class="post"]');
$res = [];
foreach ($nodes as $node) {
  $res[] = [
    'id'    => (string) $node->xpath('@id')[0],
    'date'  => (string) $node->xpath('div[@class="date"]')[0],
    'title' => (string) $node->xpath('h2')[0],
    'text'  => dom_import_simplexml($node->xpath('div[@class="body"]')[0])->textContent,
  ];
}

SimpleXML dále neumí extrahovat textové elementy, neumí získat textový obsah celého podstromu elementů a hlavně špatně vyhodnocuje XPath dotazy.

Pokud mám dokument, který vypadá takto:

<el>
  <crap>i dont' care about this</crap>
  this is really important
  <crap>this is crap</crap>
</el>

za žádnou cenu nemůžu extrahovat text „this is really important“. Ani přes API ani přes XPath dotaz. A když chci získat celý textový obsah elementu <el> včetně dětí <crap>, musím <el> importovat do DOMu a na něm zavolat textContent. Bohužel právě tohle jsou věci, které při crawlování HTML dokumentů dělám velice často. Na jedné straně tedy mám DOM, který má barokní API a na druhé straně SimpleXML, která má API velice jednoduché, ale nepoužitelné.

Právě z těchto důvodů jsem si napsal Matcher, se kterým je parsování a extrakce dat z HTML až trapně jednoduchá.

V Matcheru by výše uvedený příklad vypadal krásně deklarativně:

$m = Matcher::multi('//div[@class="post"]', [
  'id'    => '@id',
  'date'  => 'div[@class="date"]',
  'title' => 'h2',
  'text'  => 'div[@class="body"]',
])->fromHtml();

$m($htmlString);

Výsledný Matcher je obyčejná funkce, která na vstupu vezme HTML řetězec a vrátí data, jejicž tvar odpovídá tomu, jak byl Matcher deklarován.

[
  ['id' => '...', 'date' => '...', 'title' => '...', 'text' => '...'],
  ['id' => '...', 'date' => '...', 'title' => '...', 'text' => '...'],
  ['id' => '...', 'date' => '...', 'title' => '...', 'text' => '...'],
  ...
]

Pokud nechci pole polí, ale pole objektů. Stačí vzor přetypovat na objekt a výsledek bude mít požadovanou strukturu.

$m = Matcher::multi('//div[@class="post"]', (object) [
  'id'    => '@id',
  'date'  => 'div[@class="date"]',
  'title' => 'h2',
  'text'  => 'div[@class="body"]',
])->fromHtml();

Pokud chci extrahovat vnořená data, nemusím psát vnitřní smyčky, ale jenom do sebe vnořím Matchery. Kód je stále krásně deklarativní a výsledná data mají stále strukturu odpovídající vzoru.

$m = Matcher::multi('//div[@class="post"]', [
  'id'    => '@id',
  'date'  => 'div[@class="date"]',
  'title' => 'h2',
  'text'  => 'div[@class="body"]',
  'tags'  => Matcher::multi('.//div[@class="tag"]'),
  'comments' => Matcher::multi('.//div[@class="comments"]', [
    'name' => 'div[@class="name"]'
    'text' => 'div[@class="text"]'
  ]),
])->fromHtml();

Výsledek:

[
  [
    'id' => '...',
    'date' => '...',
    'title' => '...',
    'text' => '...',
    'tags' => ['...', '...'],
    'comments' => [[
      'name' => '...',
      'text' => '...'
    ], [
      ...
    ]],
  ],
  ...
]

Když je potřeba nalezené řetězce nějak upravit, můžu použít Matcher single, který nevrátí pole, ale jen první nalezený element a následně metodu map, která transformuje výsledek Matcheru požadovanou funkcí.

$m = Matcher::multi('//div[@class="post"]', [
  'id'    => Matcher::single('@id')->asInt(),
  'date'  => Matcher::single('div[@class="date"]')->map('strtotime'),
  'title' => 'h2',
  'text'  => 'div[@class="body"]',
  'tags'  => Matcher::multi('.//div[@class="tag"]'),
  'meta'  => function ($node) { return doSomethingWithDomNode($node); }
])->fromHtml();

K dispozici jsou ještě metody: asInt, asFloat, first (matcher vrátí jenom první výsledek z kolekce) nebo regex (na výsledek matcheru aplikuje regulérní výraz a to i rekurzivně).

Ve výsledku může třeba takhle vypadat crawler, který používá Matcher, Atrox\Curl a Atrox\Async a je schopný crawlovat paralelně.

flattr this!

Posted in PHP, Web | 3 Comments

Výsledky PHP kvízu

Před nějakou dobou jsem tu zveřejnil maličký PHP kvíz, teď je načase odhalit správné odpovědi.

Ty jsou následující:

  1. kolize hashů
  2. velikost CPU cache
  3. cache prefetching
  4. hashování klíčů polí a cache prefetching

První otázka byla jednoduchá a šlo pochopitelně o kolize hashů.

V PHP je všechno hash tabulka: pole, deklarované a nedeklarované proměnné objektu a dokonce i metody jsou organizovány jako hash tabulky. Jedinou výjimkou je SplFixedArray, které je interně organizované jako skutečné pole.

PHP pole (viz HashTable ve zdrojácích PHP) má po vytvoření kapacitu 8 (viz _zend_hash_init). Když do něj začnu přidávat položky a dosáhnu této hranice, PHP pole zvětší svoji kapacitu na dvojnásobek (viz funkce zend_hash_do_resize).

Stringové klíče jsou hashovány funkcí DJBX33A (viz zend_inline_hash_func), ale celočíselné klíče se použijí přímo a co víc: stringové klíče, které obsahují číslo se na tohle číslo převedou. Tohle uspořádání má překvapivý dopad na výkon: Když používám PHP pole jako skutečné pole, tedy mám jen numerické klíče bez mezer z určitého intervalu, jsou položky seřazeny přímo podle klíče. Položka s indexem 5 je tedy v paměti hned vedle položky s indexem 6 a to je dobré pro CPU cache. Kdybych klíče hashoval, sousední indexy by se nacházely na různých místech tabulky, které nijak neodpovídají jejich numerickému řazení (o tomto se ještě zmíním u čtvrté otázky).

Z tohoto důvodu pro kolizi nemusím složitě hledat klíče, jejichž hash bude kolidovat s jinými klíči (jak to před pěti lety udělal Jakub Vrána), ale prostě použiji celočíselné klíče, které přímo kolidují. Protože PHP pole začíná na velikosti 8 a zvětšuje se na dvojnásobek, stačí si vybírat klíče, které mají stejné $key % (pow(2, $n) * 8) pro nějaké dostatečně velké $n, tedy nejlépe $i * FACTOR. Já zvolil FACTOR=1048576, což odpovídá `8 * pow(2, 17)`, tedy velikosti 17× zvětšeného pole. FACTOR musí být větší než kapacita pole, jinak všechny klíče nebudou kolidovat do jednoho místa, ale do několika.

Aby se tomu předešlo, stačí zvolit jiný způsob zvětšování kapacity tabulky`. Například začít na kapacitě 10 a novou velikost určit jako $capacity << 1.

Dále k tématu útoků pomocí kolizí hash tabulek:


Druhá otázka byla o něco složitější, protože nejde o čistě záležitost PHP, ale toho, jak se chová procesor na němž PHP běží.

Příčinu snadno odhalí linuxový nástroj perf pro čtení hardwarových čítačů a statistik. Když ho spustím jako perf stat -e cache-misses -e instructions -e cycles -e LLC-prefetches php test.php u obou verzí programu bude hlásit přibližně 7.5 miliardy vykonaných instrukcí, ale ostatní čísla se budou lišit. Rozdílný bude hlavně počet cache-misses: 11.5 milionu proti 25.5 milionu. Cache-miss nastane, když procesor daný kus paměti nemá v interní cache, ke které může přistupovat velice rychle během několika málo taktů, a musí pro něj do hlavní paměti, odkud to trvá až 300 taktů. První verze kvízového programu opakovaně přistupuje k datům jenom z omezeného intervalu a tedy všechno, co potřebuje k práci, se pohodlně vejde do CPU cache. V případě druhé verze chce data z celého rozsahu a jenom pointery pro 2000000 elementů by potřebovaly 16MB paměti (plus objekty na které ukazují). Moderní procesory dostupné pro běžné smrtelníky mají maximálně 8MB L3 cache (cache má několik úrovní, které se liší velikostí a rychlostí přístupu a vždycky platí, že tyto dvě veličiny jsou nepřímo úměrné, L1 je velice malá, ale velice rychlá, L2 je průměrná, L3 velká, ale nejpomalejší), do které se nevejde celý woking set a tím pádem CPU musí číst data z pomalé hlavní paměti. Důležitý je také fakt, že kvízový program k datům přistupoval náhodně. Běžné programy, které pracují s velkým rozsahem dat typicky potřebují nějaká data častěji než jiná a cache se postará o to, aby obsahovala ten nejužitečnější podmnožinu dat.


Třetí otázka je na tom podobně jako ta druhá: pro řešení musím sestoupit úroveň pod PHP a perfem trochu prošťouchnout procesor. Tentokrát mě zajímá hlavně čítač události LLC-prefetches.

Protože přístup do hlavní paměti trvá tak strašlivě dlouho, procesory se snaží před-načítat (prefetch) data do cache, jakmile detekují sekvenční průchod pamětí. Ten je v programech velice běžný, například u smyček, které procházejí celé pole od začátku do konce, jeden element za druhým. Procesor, který prefetchuje, tak vsází na to, že v budoucnu bude chtít blok paměti, která je o kus dál. Když se strefí, potřebná data pro další iterace budou čekat v rychlé cache připravená pro čtení. RAM má sice mizerné latence, ale velice dobrou propustnost (bandwidth) v řádech desítek gigabajtů za vteřinu. Když se celá prefetch mašinerie rozjede, procesor před-načítá bloky dat tak rychle, jak je RAM dokáže servírovat a CPU je čte z cache jen s maličkými prodlevami. V některých případech prefetching zcela skryje latence hlavní paměti a pipelining a OOO i latence cache.

Prefetch je velice efektivní, ale musí mít správné podmínky k práci, tedy nějaký předvídatelný vzor průchodu pamětí. Když program nahání pointery nebo skáče na náhodná místa, prefetch nic nezmůže.

A právě tohle způsobuje rozdíl mezi oběma verzemi programu. Jeden čte sekvenčně a může využít prefetch, druhý čte náhodně a vždycky musí trpět cache-miss a latenci RAM. perf u jedné verze programu ukazuje 3.8 milionu cache-miss a 13.2 milionu LLC-prefetches a u druhého 9.8 milionu cache-miss a 4.5 milionu LLC-prefetches (LLC znamená last level cache, tedy typicky L3). Program, který víc přednačítá má menší počet cache-miss a je rychlejší.


Poslední otázka je kombinací první a třetí. V jedné verzi procházím polem, které zdánlivě vypadá, že má stringové klíče obsahující jenom čísla, ve druhé má pole stejné klíče s krátkým stringovým prefixem a ve třetí stejné klíče se stringových suffixem. První verze je nejrychlejší, druhá je pomalejší a třetí je nejpomalejší (0.24s vs. 0.36s vs. 0.46s).

Jak už jsem psal u první otázky, stringové klíče, které mají celočíselnou hodnotu se zkonvertují na integery a jejich hodnota se bere jako jejich hash. První případ je nejrychlejší proto, že klíče jsou v hashtabulce řazené podle svého numerického pořadí a při sekvenčním průchodu zapracuje prefetch. Pokud před ně přidám stringový prefix, už se musí prohnat hash-funkcí a jejich pozice v hash tabulce nebude odpovídat jejich lexikografickému pořadí a to zabrání prefetcheru dělat jeho práci.

Ale proč je verze s suffixem pomalejší než ta, která prefixuje? Překvapivě to souvisí s hash funkcí a prefetcherem.

Funkce, kterou PHP používá pro hashování klíčů, vypadá takto:

long DJBX33A(const unsigned char *key) {
        long hash = 5381;
        while(*key) hash = 33*hash + *key++;
        return hash;
}

Vezme jeden znak klíče, vynásobí 33, přičte k hashi a posune se na další. A tady leží řešení naší záhady. Když změním poslední znak o jedničku, hash se změní o 33, když změním předposlední, hash se změní o 33*33. První znak má největší dopad na změnu hashe, poslední má nejmenší. V prefixové verzi programu se inkrementuje poslední znak klíče a z jedné iterace na druhou se hash zvětší o 33, tedy o fixní počet, který odpovídá fixnímu kroku v paměti, který prefetcher může detekovat a využít. Naproti tomu, když mám indexy se suffixem, index se zvětšuje o 33*33 = 1089, což v hashtabulce odpovídá kroku 8712 bajtů a s tím si prefetcher, který umí fungovat jenom v rámci jedné stránky paměti, neporadí.

perf tuto teorii pěkně potvrzuje:

perf stat -e cache-misses -e instructions -e cycles -e LLC-prefetches php test.php

no prefix
         2 812 318 cache-misses
     6 386 561 958 instructions              #    2,33  insns per cycle
     2 745 568 076 cycles
        10 578 170 LLC-prefetches

prefix
         9 575 382 cache-misses
     6 690 346 583 instructions              #    1,73  insns per cycle
     3 874 722 279 cycles
        25 827 757 LLC-prefetches

suffix
        13 671 528 cache-misses
     6 983 481 506 instructions              #    1,54  insns per cycle
     4 534 416 656 cycles
        14 652 597 LLC-prefetches

Na výsledcích je zajímavých několik věcí: Za prvé ukazují, že i ve vysokoúrovňových interpretovaných a relativně pomalých jazycích jako PHP, jsou patrné nízkoúrovňové detaily fungování CPU a cache a dopad na výkon může být v některých případech veliký. A za druhé ukazují, že měření výkonu je zrádné. Různé procesory mají různě velké, různě uspořádané a různě rychlé cache s různou asociativitou a všechny tyto detaily můžou mít velký dopad na finální výkon programu. A co víc, naměřená čísla se můžou měnit běh od běhu programu a doslova jeden den může naprosto stejný program běžet o něco pomaleji bez zjevných důvodů.

Další čtení:

flattr this!

Posted in CPU, Paměť, PHP | 1 Comment

PHP kvíz (aktualizováno)

Dneska vám přináším malý PHP kvíz. Každou jeho část představuje krátký kus kódu, který s maličkou změnou najednou začne běžet pomaleji, i když dělá stejné množství práce. Vaším úkolem je poznat co to způsobuje a proč. Odpovědi můžete psát do komentářů.

(Update: Přidal jsem jeden nový kvíz a přidal měření na desktopovém CPU, na kterém rozdíly mnohem víc vynikají).


Na začátek něco jednoduchého:

define('FACTOR', 1);

$arr = [];
for ($i = 0; $i < 100000; $i++) {
        $arr[$i * FACTOR] = 1;
}

Pokud je konstanta FACTOR rovna jedné, smyčka na Atomu proběhne ze 40 milisekund. Pokud se FACTOR rovná 1048576, vykoná se za 84 vteřin.

Co to způsobuje a jak?


Další dvě otázky jsou už o něco zajímavější.

define('INTERVAL', 1000);

$arr = [];
for ($i = 0; $i < 2000000; $i++) {
  $arr[$i] = ['a' => 1+1];
}

$start = microtime(true);

for ($i = 0; $i < 1000000; $i++) {
  $arr[mt_rand(0, INTERVAL-1)];
}

$time = microtime(true) - $start;

Pokud má konstanta INTERVAL hodnotu 1000, poslední smyčka na netbookovém Atomu trvá 3 vteřiny, pokud má hodnotu 2000000, trvá 3.7 vteřin; na desktopovém Haswellu je to pak 0.14 vteřiny pro INTERVAL=1000 a 0.9 vteřiny pro INTERVAL=2000000. Proč je kód až 6× pomalejší, když se indexy generuji z většího intervalu? V jiném jazyku nebo virtuálním stroji by tento rozdíl mohl být ještě větší.


Třetí otázka se nese v podobném duchu.

$arr = [];
for ($i = 0; $i < 500000; $i++) {
  $arr[$i] = ["a" => 1+1];
}

$start = microtime(true);

for ($i = 0; $i < 500000; $i++) {
  $idx = mt_rand(0, 500000-1);
  $idx = $i; // tenhle řádek odstraním
  $arr[$idx];
}

$time = microtime(true) - $start;

Poslední smyčka na netbookovém Atomu proběhne za 1.5 vteřiny, když odstraním řádek $idx = $i, kód běží o víc jak 20% pomaleji (1.8s). Na Haswellu se smyčka vykoná za 0.11 vteřin a pokud odstraním onen řádek, tak za 0.4 vteřiny, tedy skoro čtyřikrát pomaleji. Proč, i když dělám stejné množství práce, ale index se neinkrementuje o jedničku, všechno déle? I tady platí, že v jiném jazyce nebo jiném prostředí by rozdíl mohl být mnohokrát větší.


Dodatečně přidám ještě jednu kvízovou otázku.

$arr = [];
$prefix = ""; // prefix mastavím na "x"
for ($i = 0; $i < 2000000; $i++) {
        $arr[$prefix.$i] = $i;
}

$start = microtime(true);

for ($i = 0; $i < 2000000; $i++) {
  $arr[$prefix.$i];
}

$time = microtime(true) - $start;
echo $time, "\n";

Pokud je proměnná prefix prázdný string, poslední smyčka trvá 0.24 vteřiny, pokud se $prefix rovná stringu „x“, trvá 0.36 vteřiny, ale když místo prefixu použiji postfix (indexace přes $arr[$i.$prefix]), trvá 0.46 vteřiny. Proč se tak děje?

Odpovědi pište do komentářů, za pár dnů tady zveřejním správné odpovědi.

flattr this!

Posted in Paměť, PHP | 6 Comments

Haldy nejsou tak velké, jak se se zdají být

Nedávno jsem narazil na zajímavý test, který se snažil nahrubo určit kolik paměti potřebují různé datové struktury na JVM: kolekce z Javy, Scaly, Googlí Guava a kolekce Trove specializované pro primitivní typy. Test probíhal tak, že nastartoval JVM s 1GB heap, vytvořil danou kolekci, začal přidávat jeden element za druhým až do okamžiku, kdy došla paměť a virtuální stroj vyhodil OutOfMemoryError. Potom autoři prohlásili, že daná kolekce s výsledným počtem elementů zabere plus/mínus jeden gigabajt paměti.

To všechno vypadalo celkem rozumně až do okamžiku, kdy jsem si prošel výsledky. Z nich vyplývalo, že do jednoho gigabajtu paměti se vejde Trove TIntArrayList (který ukládá neboxované integery) o délce nejvýše 10485760 elementů. Neboli 40 megabajtů dat.

Tady něco nehraje. Čekal jsem, že to bude desetkrát až dvacetkrát více.

Hned jsem začal vrtat v implementaci Trove kolekcí, zdrojácích testu a měřit velikosti objektů a za chvíli jsem zjistil, že Trove TIntArrayList je implementován polem, které má výchozí kapacitu 10 a když se naplní, alokuje větší pole, překopíruje do něj starý obsah a přidá nová data. Velikost nového pole se rovná staré velikost posunuté o jeden bit doleva. A 10 << 20 je právě 10485760, což znamená, že test narazil na OOM, když TIntArrayList prováděl jednadvacáté zvětšení. V ten okamžik se na haldě nacházelo staré pole délky 10M a snaha alokovat nové o délce 20M selhala. Ale i když sečteme velikosti těchto dvou polí, dostaneme pouhých 120MB dat na 1GB haldě než JVM začne kapitulovat.

Tady pořád něco nehraje.

Nastartoval jsem tedy VisualVM, abych se podíval, co se ve virtuálním stroji doopravdy děje a hned mě to udeřilo do očí: generační garbage collector.

Pokud JVM používá tento GC, rozdělí haldu do několika oblastí: eden, survivor a old a žádná z nich není dost velká na to, aby pojala gigantické pole. A protože data nemůžou přečnívat z jedné oblasti do druhé, není možné alokovat pole větší než nejmenší z těchto regionů, což bude nejspíš survivor, který je tvořen dvěma nezávislými polovinami mezi kterými se kopírují data.

Tato teorie také vysvětluje, proč „fragmentované“ kolekce, které nepotřebují velká souvislá pole, mají v testu naměřenou poměrně velkou kapacitu, která je rozhodně větší než by odpovídalo rozdílu mezi boxovanými a primitivními typy.

Nejsnadnější způsob, jak tento problém řešit, když na něj narazíte, je nastavit větší heap a dál se o nic nestarat.

flattr this!

Posted in JVM, Paměť | 5 Comments