title 0xDEADBEEF baseUrl https://deadbeef.k47.cz language cs outDir /home/drgonzo/k47deadbeef remoteDir /home/drgonzo/mnt/k47deadbeef/ fullArticlesOnIndex 10 groupArchiveBy archiveFormat link limitRss 15 fullArticlesInRss true articlesMustBeSorted true articlesMustNotBeMixed true style| .b { font-family: sans-serif; font-size: 0.9em; line-height:1.5; max-width: 50em; margin: 2em 1em; padding: 1.5em; background-color: white } style| body { background-color: #222 } style| h1 { font-size:1em; display:inline; color:white;background:#222;padding:0.2em 0.4em } style| a:hover { color:red } style| a { background-color:#FFdde9 } style| ::-moz-selection { background-color:pink } style| pre { background-color: #efe9e9; padding: .4em; margin: -0.4em 0em; font-size: 1.1em; } footer
píše k47 (@kaja47)
fileSuffix .html header

0xDEADBEEF

RSS
allowComments true shareLinks true twitter.site @kaja47 tagFormat short inline! Co vlastně benchmarkujete? [php-benchmark] ===== 2018-08-04 # PHP, VM Před nějakou dobou jsem narazil na "článek, který se snažil testovat rychlost a paměťové nároky asociativních polí a různých variant objektů v PHP":[b]. Závěr byl ten, že objekty jsou skoro ve všech ohledech buď srovnatelné nebo lepší než asociativní pole, hlavně co se týká paměťových nároků[[1]]. Ale stačil jediný pohled, abych věděl, že je něco špatně. Benchmark asociativních polí vypadal takhle: /---code random_int(1, 500), 'b' => base64_encode(random_bytes(16)), ]; } ksort($list); usort($list, function($first, $second) { return [$first['a'], $first['b']] <=> [$second['a'], $second['b']]; }); $stop = microtime(true); $memory = memory_get_peak_usage(); printf("Runtime: %s\nMemory: %s\n", $stop - $start, $memory); \--- Co tento kus kódu měl měřit? Vytváření asociativních polí (v dalších variantách pak objektů) a pak práci s nimi, přece. Ne tak docela. Děje se toho podstatně víc. Vytváření polí, volání funkce `random_int`, `random_bytes`, `base64_encode`, pak řazení podle klíče, která je více méně noop, další řazení podle obsahu interních struktur a alokace dočasných polí. Na mém Haswell stroji s PHP 7.2.4 měřený úsek běží 8 vteřin. Znamená to, že to co mě zajímá trvá 8 vteřin? Ne, celá ta věc trvá osm vteřin. Kolik z nich jsem strávil měřením rychlosti přístupů do polí/objektů? To není bez podrobnějšího zkoumání možné říct. Začal jsem se proto vrtat, kolik času zaberou jednotlivé fáze a výsledky jsou zajímavé ("kdyby nebyly, nic bych nepsal":[p]). PHP je naštěstí primitivní runtime, který se nesnaží o chytré optimalizace a měření je proto jednoduché ("jak říkal Aleksey Shipilёv":[sh]). Můžu například udělat v podstatě jakoukoli změnu a nebát se, že "chytrý JIT kompletně eliminuje veškerý kód bez pozorovatelných efektů":[mb]. Samotná konstrukce polí/objektů je v podstatě zdarma. `ksort` řadí podle již seřazeného numerického klíče, je také zadarmo a nepřináší žádnou užitečnou informaci o chování datových struktur. A pak jsou tu také funkce `random_int`, `random_bytes`, `base64_encode`. První dvě generují kryptograficky bezpečná náhodná čísla, což nejspíš nebude zadarmo. A také není. Jen tyto funkce zaberou jednu vteřinu času, tedy ±12% zbytečné režie, která opět neříká nic užitečného. Teď poslední řazení. Porovnávací funkce uvnitř využívá *spaceship* operátor `<=>` mezi dvěma nově vytvořenými poli. To je velice elegantní, ale dočasné datové struktury je třeba alokovat a i když každá alokace je levná (viz pár odstavců výše), provádí se jich O(nlog(n)) a to se nastřádá. Když to změním na /---code usort($list, function($first, $second) { $cmp = $first['a'] <=> $second['a']; if ($cmp !== 0) return $cmp; return $first['b'] <=> $second['b']; }); \--- test běží o 3 vteřiny rychleji. Nejde jen o případ optimalizace benchmarku, ale důkaz, že ±37% času tráví alokacemi, dealokacemi a počítáním referencí, tedy ne tím, co měl měřit. Dohromady tráví 50% času zbytečnou režií a výsledky jsou v ní utopené. Závěry benchmarku o relativních rychlostech polí a různých objektů jsou v tomto případě stále platné, jen poměry se liší. Režie je částečně maskovala a bez ní jsou přibližně dvakrát větší. Benchmarkování je komplikované a takhle by se to nemělo dělat. Je nutné izolovat měřený efekt a i tak čísla jsou pouhá data, bez pochopení, proč jsou taková jaká jsou, nemají význam. Říct "X je z nějakého důvodu rychlejší než Y a proto bychom ho měli používat", není dobrý nápad. Jak "varuje JMH":[j]: *Do not assume the numbers tell you what you want them to tell.* --- 1) Ale i toto je možné optimalizovat. Virtuální stroj může asociativní pole se stejnou strukturou zkompilovat do podoby, která je interně k nerozeznání od objektů. "Self":[s] to začal dělat v osmdesátých letech a dnes na tom stojí všechny moderní JavaScriptové VM. Pokud se chcete dozvědět víc, sledujte "tento kurz":[k]. [b]: https://steemit.com/php/@crell/php-use-associative-arrays-basically-never [mb]: http://funkcionalne.cz/2017/04/mikrobenchmarky-jsou-tezke/ [s]: https://en.wikipedia.org/wiki/Self_(programming_language) [k]: http://www.wolczko.com/CS294/ [p]: https://en.wikipedia.org/wiki/Publication_bias [sh]: https://www.youtube.com/watch?v=VaWgOCDBxYw [j]: http://hg.openjdk.java.net/code-tools/jmh/rev/2985092a3a59 Java, Scala a regulární výrazy #5 - posesivní regexy a StackOverflowError [regex-5] ===== 2018-07-28 # Java, Scala, regex "Minule jsem psal o hladových a líných regexech":[r4], naznačil jejich implementaci a ukázal případ, kdy líný regex může předejít přetečení zásobníku a `StackOverflowError`. V java implementaci regexů existuje ještě třetí druh opakování, vedle *greedy* a *lazy* (hladových a líných), jsou ještě *possesive* regexy. Zapisují se jako `.*+`, `.++`, `.?+` a `.{m,n}+`, jsou hladové, nikdy nedělají backtracking a v případech, kdy by obyčejné hladové opakování backtrackovalo, selžou. Například regex (ve Scale string se třemi uvozovkami je brán jak je, není třeba v něm skoro nic escapovat, ideální pro regexy) /---code """(?x) " ( \\" | [^"] )++ " """.r \--- začne hledat od první dvojité uvozovky, hladově konzumuje všechno, co je escapovaná uvozovka nebo jakýkoli jiný znak kromě uvozovky, až narazí na ukončovací uvozovku a když ji najde, nikdy neustoupí o krok zpět. Chová se tedy do jisté míry atomicky (kdo neví, tak `(?x)` na začátku znamená, že mezery se ignorují). Posesivní regexy mohou vést k odmítnutí některých stringů, které by hladové nebo líné regexy přijaly. Ale na druhou stranu regex selže brzo a nebude zkoušet všechny permutace. Jde tedy o druh optimalizace. Často právě posesivní regexy jsou to, co člověk chce. Hodí se, když chci hledat až do určitého symbolu, který jasně indikuje, kde posesivní regex skončí. Často mívají formu `a [^a]++ a`. Protože nedělají backtracking jsou implementovány iterativně a nemůžou skončit StackOverflowError. Například následující dva regexy testují to samé. /---code ("a " * 1000000).matches("(a+ )+") ("a " * 1000000).matches("(a+ )++") \--- První skončí StackOverflowError, ale druhý doběhne v pořádku. (Kdo neví, tak násobení stringu ve Scale ho opakuje.) Navíc: Protože nedělají backtracking, logicky u nich nehrozí případy katastrofického backtrackingu. Opět dva případy: První je obyčejný hladový regex, druhý je *possesive* regex. /---code "ACCCCCCCCCCCCCCCCCCCCCCCCCCCCCCX".matches("A(B|C+)+D") "ACCCCCCCCCCCCCCCCCCCCCCCCCCCCCCX".matches("A(B|C++)+D") \--- Druhý dojede v mikrosekundě, ale první bude trvat celou věčnost právě proto, že začne zkoušet všechny možnosti a dojde ke kombinatorické explozi. Posesivní regex bude vyhodnocován takto: `A` pasuje, zkusí `B`, nic, `C++` sedne a sebere všechna `C`, zkusí `D`, selže, `++` zajistí, že nevrátí žádné `C` zpátky, nezkusí další varianty a celý regex selže. --- Pozn: - Jak jsem četl zdrojové kódy regexů, myslím, že by nebylo těžké napsat knihovnu, které provádí reguláry nad libovolnými kolekcemi a ne jen řetězci stringů. [s]: https://github.com/dmlloyd/openjdk/blob/jdk10/master/src/java.base/share/classes/java/util/regex/Pattern.java [r4]: regex-4 EDGE, TRIPS a hon za vyšším ILP [edge] ===== 2018-07-26 # CPU Tiskem proletěla zpráva, že "Microsoft portoval Windows a Linux na vlastní experimentální CPU architekturu E2":[ms]. Pročetl jsem pár publikací o architektuře a na pohled vypadá zajímavě. Tedy aspoň z technického pohledu je ambiciózní, jestli povede k reálnému produktu, si netroufám odhadovat. To nejlepší na začátek: Microsoft se rozhodl vytvořit "*dataflow* stroj":[df]. I když ne tak úplně, jde o omezený *dataflow* v technickém žargonu označovaný "EDGE":[e] - *Explicit data graph execution*. Článek "An Evaluation of the TRIPS Computer System":[ev] poskytne detaily o prvotní EDGE architektuře "TRIPS":[tr], vyvíjené v akademickém prostředí. Její autoři se snažili dosáhnout vysokého ILP (instrukční paralelismus) v malém a úsporném procesoru a mít všechny výhody *out-of-order* bez křemíkové a energetické režie OoO. CPU provádí bloky až 128 instrukcí bez skoků (kompilátor se je snaží nahradit "predikací":[pr]). Blok commitne atomicky jako celek a uvnitř se chová jako *dataflow*. Instrukce nezapisují výsledky do registrů, ale přímo je směřují závislým instrukcím. Pokud je blok dostatečně velký, může obsahovat velké množství instrukčního paralelismu (TRIPS jádro má 16 ALU) a díky jeho explicitní *dataflow* povaze je prováděn jako na OoO jádře. Závislosti mezi instrukcemi nemusí za běhu analyzovat procesor, ale jsou identifikovány už v procesoru. Pro představu to může vypadat nějak takhle (hypotetický pseudokód): /---code i1: READ R1 i3 // přečte z globálního registru a data nasměruje instrukci i3 i2: READ R2 i3 i4 // to samé, ale pošle je instrukcím i3 a i4 najednou i3: ADD i4 // až obdrží data, sečte je a výsledek pošle instrukci i4 \--- Toto uspořádání řeší problémy, kterými trpěly obecné *dataflow* architektury. Ale jako obvykle komplikované větvení kódu se nekamarádí s ILP, protože vede k malým blokům instrukcí. Paper "Dynamic Vectorization in the E2 Dynamic Multicore Architecture":[dv] představuje další pokrok již pod taktovkou Microsoftu. Jde o první nástřel z roku 2010, ale přesto ukazuje několik zajímavých inovací, jako například dynamické spojování několika fyzických jader do jednoho logického. Jedno fyzické jádro v gangu provádí nespekulativní kód a ty ostatní jsou použité pro spekulace. Po spojení jader tak vznikne agresivní superskalární OoO CPU. To dodá procesoru flexibilitu - na jedné straně v něm může běžet mnoho malých jader na druhé několik málo velice mocných jader pro jednovláknové programy. Další chuťovkou je vektorový mód, kterého je dosaženo kombinací všech skalárních ALU v jádře. Nepotřebuje tedy novou sadu registrů nebo nové funkční jednotky, ale používá všechny existující hardwarové prostředky. Všechno tohle EDGE šílenství je honem za stále ILP - TRIPS měl 16 ALU a výzkumníci dospěli k závěru, že ideální stroj s nekonečným množstvím funkčních jednotek a masivním instrukčním blokem by dokázal provádět desítky až stovky instrukcí každý takt. Jeden z prototypů E2 měl údajně mít 32 ALU. Za ILP se dříve hnalo Itanium a všichni víme jak to s tímto dítkem Intelu a HP dopadlo. O podobný kousek se snaží i "architektura Mill":[m], ale jde cestou kompletně statického VLIW. Historie nedává příliš optimismu, že by to mohlo vyjít. Jedním z mnoha problémů Itania byl fakt, že potřebovalo pokročilý kompilátor, který by dokázal ohnout kód tak, aby sedl na masivně paralelní spekulativní predikované srdce čipu. Stejně to budou potřeboval EDGE (a Mill) procesory. Pokud nebudou k dispozici, můžou skončit jako produkty s nevyužitým potenciálem. Ale také nemusí, když končí moorův zákon, efektivita, i když v porovnání s exponenciálními zisky minulosti jen marginální, může hrát značnou roli. --- K tématu: - "Von Neumannovy lži":[http://funkcionalne.cz/2017/05/von-neumannovy-lzi/] - "Dualismus hardwaru a softwaru, strojů a virtuálních strojů":[http://funkcionalne.cz/2016/03/dualismus-hardwaru-a-softwaru-stroju-a-virtualnich-stroju] - "Čím více se věci mění, tím více zůstávají stejné":[http://funkcionalne.cz/2016/05/cim-vice-se-veci-meni-tim-vice-zustavaji-stejne] - "Úvod do podivností moderního hardwaru, které vás budou budit ze spaní":[http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani] - "Za jak dlouho procesor vynásobí tisíc čísel":[http://funkcionalne.cz/2015/05/za-jak-dlouho-procesor-vynasobi-tisic-cisel] [ms]: https://www.theregister.co.uk/2018/06/18/microsoft_e2_edge_windows_10/ [df]: https://en.wikipedia.org/wiki/Dataflow_architecture [e]: https://en.wikipedia.org/wiki/Explicit_data_graph_execution [ev]: https://www.cs.utexas.edu/users/mckinley/papers/trips-eval-asplos-2009.pdf [tr]: https://www.cs.utexas.edu/~trips/prototype.html [dv]: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/e2-heart2010.pdf [pr]: https://en.wikipedia.org/wiki/Predication_(computer_architecture) [m]: https://www.youtube.com/playlist?list=PLFls3Q5bBInj_FfNLrV7gGdVtikeGoUc9 [mz]: konec-intelu-a-moorova-zakona Striktní a líné jazyky [striktni-a-line-jazyky] ===== 2018-06-24 # Scala, FP "František Řezáč":[fr] napsal článek o tom, že "Operátory nejsou (vždy) funkce":[op]. To je pravda a není k tomu třeba dodávat nic víc. Stojí to ale za pozornost jen v případě striktních jazycích typu C a Java, kde jsou argumenty funkce vyhodnoceny vždy před jejím zavoláním ("*applicative order*/*call by value*":[e]). V takovém prostředí není jednoduše možné napsat vlastní funkci OR nebo AND, která se chová stejně jako vestavěné operátory `||` nebo `&&`. Ty v klasické sortě céčku podobných jazyků vyhodnotí druhý argument jen pokud je to nezbytně nutné. Toho bez funkcí vyššího řádu (jak FR podotknul) není možné dosáhnout. Proto v nich operátory zaujímají speciální místo. Mě to koplo nedávno, když jsem psal interpret jednoduchého lipovského jazyka[[1]], kde odloženého vyhodnocování bylo nezbytné k tomu, aby následující kód neskonšil `NullPointerException`. /---code (if (and (nil? x) (.nonEmptx x)) (something)) \--- V líných jazycích nebo těch, které podporují *call-by-name* nebo *call-by-need*, operátory v principu nemají speciální postavení[[2]] a jsou to jen další metody. Ve Scale můžu napsat vlastní funkci OR takto: /---code def OR(a: Boolean, b: => Boolean): Boolean = if (a) true else b \--- Typ `=> Boolean` označuje *call-by-name* boolean. `b` není vyhodnoceno při zavolání funkce, namísto toho bude vyhodnoceno na každém místě, kde je v těle funkce použito. Kompilátor *call-by-name* parametr zabalí do anonymní funkce, která je pak opakovaně volána. Nejde ale o plně líné neboli *call-by-need* vyhodnocování jako například v Haskellu. V něm je nevyhodnocený výraz reprezentován strukturou "běžně nazývanou *thunk*":[t], která ukazuje na kód. Když je přinucena se vyhodnotit (například pattern matchingem), je odkazovaný program vykonán a thunk je atomicky zaměněn na výsledek. Líné jazyky mají tu vlastnost, že nevyhodnocené nemusí být jen argumenty funkce, ale celé datové struktury. Můžu mít masivní strom, ale když jsem četl jen z jedné cesty k listům, všechny nenavštívené odbočky zůstanou jako nevyhodnocené *thunk*y. *Call-by-need* je tedy *call-by-name* doplněné s memoizací. Ve Scale je to možné emulovat takto: /---code def f(_a: => X) = { lazy val a = _a ??? } \--- Když někde použiji proměnnou `a` dojde k jejímu vyhodnocení jen jednou a výsledná hodnota se bude recyklovat při každém dalším použití. --- Poznámky: - "Dijkstra upozornil, že *short-circuit* operátory porušují pravidla distributivity":[d] a měli bychom se jim vyhýbat, neboť ztěžují porozumění programu. --- 1) Jmenoval se Lispy a byl zamýšlený jako skriptovací nástroj pro "asciiblog":[a]. Psalo se v něm příjemně, ale nakonec jsem ho nahradil za javascript vestavěný do Javy. 2) Kompilátor o nich ví a proto bych si dovolil odhadovat, že pro ně vygeneruje lepší kód. [fr]: https://twitter.com/calaverainfo [op]: https://calavera.info/v3/blog/2018/05/21/operatory-nejsou-funkce.html [a]: https://github.com/kaja47/asciiblog [e]: https://en.wikipedia.org/wiki/Evaluation_strategy [t]: https://en.wikipedia.org/wiki/Thunk [d]: https://en.wikipedia.org/wiki/Short-circuit_evaluation Syntéza datových struktur a programů [syntetizace] ===== 2018-06-21 "Generalized data structure synthesis":[g] stojí za přečtení. Prezentuje nástroj Cozy, který - jak říká název článku - umí syntetizovat datové struktury operace nad nimi, které jsou nejen korektní, ale také efektivní. Nechci opakovat, co už shrnul Adrian Colyer, přečtěte si to, teď hned, je to důležité pro následující odstavce. Jde o perfektní doplněk k "Out of the Tar Pit":[t] a možná chybějící článek nutný k tomu, aby vize načrtnutá v *tar pit* paperu (pokud nechcete číst, "tady jsem ho kdysi shrnul":[f]). Autor *tar pit* paperu mluví o esenciální a vedlejší komplexitě, přičemž ta esenciální je ta důležitá, jde o nutnou složitost problému nebo domény, chcete-li. Vedlejší komplexita nemá žádnou hodnotu pro uživatele, jen je někdy nutná pro dosažení efektivity a dostatečného výkonu. Esenci musíme nějak vyjádřit a vedlejší složitostí bychom se měli vyhnout. Na tom není nic kontroverzního, ale teď jak toho dosáhnout. V *tar pit* paperu se mluví o logickém a funkcionálním programování, deklarativním stylu a relačním datovém modelu. A tady do hry vstupuje Cozy - nadefinuji nezbytný stav bez ohledu na efektivní reprezentaci, tak jak o něm přemýšlejí uživatelé, deklarativně popíšu operace nad ním, jako v *tar pit* utopii a Cozy, nebo nějaký jeho pokročilý potomek, z této kostry syntetizuje její efektivnější verzi - efektivní datové struktury specializované pro dané operace a efektivní kód. Z esenciálního popisu vygeneruje všechnu vedlejší komplexitu a odvozený stav. --- Heuristiky a strategie prohledávání stavového prostoru, můžou být pochopitelně rozšířeny strojovým učením. [g]: https://blog.acolyer.org/2018/06/21/generalized-data-structure-synthesis/ [t]: https://github.com/papers-we-love/papers-we-love/raw/master/design/out-of-the-tar-pit.pdf [f]: http://funkcionalne.cz/2012/12/out-of-the-tar-pit/ ISPC, SPMD a SIMD [ispc-spmd-simd] ===== 2018-06-09 # CPU, SIMD "Maxime Chevalier tweetla odkaz":[m] na zajímavou sérii článků mapující počátky a vývoj kompilátoru ISPC (*Intel "SPMD":[sp] program compiler*) + do toho "vysvětlí jak funguje SPMD programovací model":[s], který je využívaný pro programování grafických karet. - "The story of ispc: origins (part 1)":[http://pharr.org/matt/blog/2018/04/18/ispc-origins.html] - "The story of ispc: volta is born (part 2)":[http://pharr.org/matt/blog/2018/04/19/ispc-volta-is-born.html] - "The story of ispc: going all in on volta (part 3)":[http://pharr.org/matt/blog/2018/04/20/ispc-volta-going-all-in.html] - "The story of ispc: C's influence and implementing SPMD on SIMD (part 4)":[http://pharr.org/matt/blog/2018/04/21/ispc-volta-c-and-spmd.html] - "The story of ispc: first benchmark results (part 5)":[http://pharr.org/matt/blog/2018/04/22/ispc-volta-first-results.html] - "The story of ispc: first users and modern CPUs coming through (part 6)":[http://pharr.org/matt/blog/2018/04/23/ispc-volta-users-and-ooo.html] - "The story of ispc: Bringing up AVX and giving something back to LLVM (part 7)":[http://pharr.org/matt/blog/2018/04/25/ispc-volta-avx.html] - "The story of ispc: more on optimizations and performance (part 8)":[http://pharr.org/matt/blog/2018/04/26/ispc-volta-more-on-performance.html] - "The story of ispc: the open source release and the end of volta (part 9)":[http://pharr.org/matt/blog/2018/04/27/ispc-volta-open-source.html] - "The story of ispc: spreading the world and leaving Intel (part 10)":[http://pharr.org/matt/blog/2018/04/28/ispc-talks-and-departure.html] - "The story of ispc: retrospective (part 11)":[http://pharr.org/matt/blog/2018/04/29/ispc-retrospective.html] Všechno začalo s "*Larrabee*":[l] - prototypem procesoru určeném pro segment, který okupuje GPGPU. Samotné první *Larrabee* skončilo jen u prototypu a neprodávalo se. Následující mikroarchitektury *Knights Corner* (57-61 jader) a *Knights Landing* (64-72 jader), hromadně nazývané produktovým jménem "*Xeon Phi*":[xp] si však úspěšně našly místo v superpočítačích. Larrabee a jeho následovníci byli navrženi jako čip, ktrerý nese velké množství jednoduchých in-order jáder[[1]] obohacených o široké SIMD jednotky + 4x"SMT":[ht]. SIMD od počátku mělo šířku 512 bitů (tedy tolik jako AVX-512, které se později dostalo do obyčejných Skylake čipů). Taková šířka znamená, že jedna SIMD instrukce pracuje s 16 čtyřbajtovými inty/floaty. To je hodně práce v jedné operaci. Teď jak tento spící potenciál využít? Nejde jen o Larrabee určené pro superpočítače, ale i běžné čipy. Když bylo uvedeno AVX, "došlo ke zdvojnásobení šířky SIMD jednotek (z 128 butů na 256 bitů) a tedy k ±zdvojnásobení výpočetní síly":[mz]. Další zdvojnásobení přichází teď s poslední generací procesorů Intel, které podporují AVX-512. Jak podotýká autor odkazovaných článků, takové zrychlení jsme viděli naposledy v devadesátých letech. Jednou možnost je automatická vektorizace kódu - ta funguje, ale nejde o programovací model, je to jen optimalizace na kterou se uživatel nemůže na 100% spolehnout. Další alternativou je psát přímo v assembleru nebo "za pomocí *intrinsic* funkcí. To ale není příliš produktivní způsob práce.":[fm]. Další varianta je pak SPMD - *single program multiple data*. Jde o to, že se paralelní program rozřízne podélně a místo abych se zdržoval explicitním paralelismem, píšu sériový program, který pracuje s jedním elementem vstupu. Mnoho instancí tohoto programu pak může běžet v mnoha vláknech, ale také můžou být přeloženy pro SIMD. V takovém případě SIMD jednotka s 16 lajnami bude provádět 16 instancí programu vedle sebe. Je to jako kdyby 16 vláken pochodovalo zcela identickým rytmem, jednu instrukci za druhou. Překlad je celkem triviální, ale poněkud nezvyklý. V přítomnosti podmínek, smyček, `goto`, `break` a `continue`, je nutné některé SIMD lajny vymaskovat jako neaktivní a tak podobně. Ve své podstatě jde o glorifikovanou funkci `map`. --- Relevantní čtení: - "Von Neumannovy lži":[http://funkcionalne.cz/2017/05/von-neumannovy-lzi/] - "Závislost je špatná (pro vaše programy i pro váš hardware)":[http://funkcionalne.cz/2017/01/zavislost-je-spatna-pro-vas-program-i-pro-vas-hardware/] - "Úvod do podivností moderního hardwaru, které vás budou budit ze spaní":[http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani/] - "Někdy je nejchytřejší nedělat nic chytrého (další kapitola nekonečného příběhu o optimalizaci)":[http://funkcionalne.cz/2016/03/nekdy-je-nejchytrejsi-nedelat-nic-chytreho-dalsi-kapitola-nekonecneho-pribehu-o-optimalizaci/optimalizaci] - "ispc: A SPMD Compiler for High-Performance CPU Programming":[http://pharr.org/matt/papers/ispc_inpar_2012.pdf] - "Sierra: A SIMD Extension for C++":[http://compilers.cs.uni-saarland.de/papers/lhh14.pdf] --- 1) Tyto jádra vycházejí z originálních "Pentií":[p], stejně jako z nich vycházely rané Atomy. [m]: https://twitter.com/Love2Code/status/1004370909170487296 [s]: https://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/lectures/07_gpucore.pdf [l]: https://en.wikipedia.org/wiki/Larrabee_(microarchitecture) [p]: https://en.wikipedia.org/wiki/P5_(microarchitecture)#P54C [sp]: https://en.wikipedia.org/wiki/SPMD [xp]: https://en.wikipedia.org/wiki/Xeon_Phi [mz]: http://funkcionalne.cz/2013/11/poznamka-k-moorovu-zakonu-a-rychlosti-procesoru/ [fm]: http://funkcionalne.cz/2017/06/horici-kremik-nasobeni-matic/ [ht]: http://funkcionalne.cz/2015/01/hyper-threading-aneb-jak-sakra-muze-bezet-vic-vlaken-na-jednom-jadre/ Konec Intelu, konec Moorova zákona, konec světa takového jaký známe [konec-intelu-a-moorova-zakona] ===== 2018-04-06 # CPU Apple oznámil, že plánují opustit procesory Itelu a chtějí přejít k vlastním čipům. Zatím se neví, jakou architekturu zvolí, zda-li to bude ARM nebo nějaká proprietární ISA. Všichni však blahořečí Apple a vyzdvihují, že jejich mobilní procesory jsou v jednovláknovém provozu stejně rychlé jako křemík od Intelu. Když to čtu, skřípu zuby. Samozřejmě, že mají stejně výkonné čipy, bylo by mnohem víc neobvyklé, kdyby je neměli. To co předvádí Apple ve svých A11 nebo Intel v Xeonech je velice blízko nejzazšího maximum. Za současných podmínek není možné z jednoho vlákna vytěžit nic víc. Procesor běžící na 2-3 GHz, který dokáže v ideálním případě provést 4 instrukce každý takt (IPC) představuje limit. Nic lepšího křemíkový průmysl nesvede vyprodukovat. Tedy aspoň co se obecného kódu týká. Vyšší takty vedou k obrovsky narůstajícím ztrátám tepla, proto ty 2-3 GHz, a nespecializovaný kód neobsahuje instrukční paralelismus, který by bylo možno vytěžit, proto ~4 IPC. Intelovské procesory mají praktické maximální IPC někde mezi 4-5. Power od IBM nebo Sparc od Oraclu jsou širší, plánované pro odpravení 8 instrukcí každý takt, ale to je jen kvlůli "8x SMT (tedy to, čemu Intel říká hyperthreading)":[ht]. Některé z těchto instrukcí můžou být "SIMD, které dokáží o něco navýšit výkon":[simd]. SIMD ale začínají přesahovat do sféry specializovaných programů, které provádějí masivní množství výpočtů, ne typický *general purpose* kód plný podmínek a větvení. Pro tyto speciálních trapně paralelní případy už máme ideální nástroj: GPU. Příslib architektury Mill o tom, že statický velice široký VLIW stroj dokáže vydolovat nebývalý instrukční paralelismus (ILP), se setkává s oprávněnou skepsí. Když už se jim podaří uspět, bude to spíš v oblasti MIPS na watt. Specializace pro danou mikro-architekturu a jádra bez masivní OOO mašinérie (ale zato s obrovským počtem funkčních jednotek), může být energeticky efektivní. Je ale stále ve hvězdách jestli se něco ze slibů zcela nové architektury skutečně vyplní. Jednovláknový IPC se moc nezlepší a nezáleží kolik plochy čipu tomu věnujeme, změna bude vždy jen v jednotkách procent. Zcela jisté je, že "budoucnost bude patřit specializovanému hardwaru":[s]. Jinak není možné dosáhnout žádného razantního zvýšení efektivity. Jsme na konci jedné éry. Všichni mají nejlepší procesory na světě a nestojí to za řeč. Teď je potřeba udělat další velký krok "do divočin, kde už neplatí Moorův zákon":[s]. --- Pozn: - Intel nejvíce profituje ze serverového trhu, kam prodává velký silikon. V této oblasti slibuje několik zajímavostí jako mnohajádrový je Xeon Phi nebo Xeony s FPGA. - Pro ukázku bona fide "specializovaného hardwaru se podívejte třeba na TPU":[t]. - Doufám, že uspěje "otevřená architektura RISC-V":[r], která je na tuto budoucnost připravovaná. --- Relevantní čtení: - "Čím více se věci mění, tím více zůstávají stejné":[http://funkcionalne.cz/2016/05/cim-vice-se-veci-meni-tim-vice-zustavaji-stejne/] (historická perspektiva) - "Závislost je špatná (pro vaše programy i pro váš hardware)":[http://funkcionalne.cz/2017/01/zavislost-je-spatna-pro-vas-program-i-pro-vas-hardware/] (závislosti limitují užitečné ILP) - "Za jak dlouho procesor vynásobí tisíc čísel":[http://funkcionalne.cz/2015/05/za-jak-dlouho-procesor-vynasobi-tisic-cisel/] - "Úvod do podivností moderního hardwaru, které vás budou budit ze spaní":[http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani/] - "Von Neumannovy lži":[http://funkcionalne.cz/2017/05/von-neumannovy-lzi/] - "Dualismus hardwaru a softwaru, strojů a virtuálních strojů":[http://funkcionalne.cz/2016/03/dualismus-hardwaru-a-softwaru-stroju-a-virtualnich-stroju/] [ht]: http://funkcionalne.cz/2015/01/hyper-threading-aneb-jak-sakra-muze-bezet-vic-vlaken-na-jednom-jadre/ [simd]: http://funkcionalne.cz/2013/11/poznamka-k-moorovu-zakonu-a-rychlosti-procesoru/ [s]: https://www.eejournal.com/article/fifty-or-sixty-years-of-processor-developmentfor-this/ [t]: https://www.youtube.com/watch?v=fhHAArxwzvQ [r]: https://en.wikipedia.org/wiki/RISC-V Meltdown, Spectre a branch prediction [meldown-spectre-branch-prediction] ===== 2018-01-31 # CPU Na Poslední Sobotě, kde jsem mluvil o Meltdown a Spectre ("slajdy zde":[sl]), jsem dostal dotaz jak jsou implementovány *branch predictory*. Něco jsem odpověděl, ale z trhanců vzpomínek mi nepřipadá, že šlo o uspokojivou odpověď. Proto jsem se rozhodl o tom něco málo napsat a hlavně poskytnout odkazy na autoritativní zdroje. O *branch prediction* (BP) jsem se zmiňoval na funkcionálně.cz "tady":[fbp] a trochu i "tady":[fbh]. Šlo ale jenom o dopady BP na výkon, ne detaily nebo principy implementace. O tom se něco dá vyčíst z "článku na wikipedii":[w]. Mnohem lepší úvod však "poskytuje Dan Luu na svém blogu":[d]. Pokud vás toto téma zajímá, rozhodně si jeho článek přečtěte. ("Už jsem ho tu vychvaloval":[b]) Zajímavý ja také paper "The YAGS Branch Prediction Scheme":[y], který prezentuje, jak může vypadat o něco sofistikovanější, ale přesto klasická implementace. Branch predictor nemusí být implementován jen pomocí tabulek historie skoků, ale i neuronovými sítěmi. Tím se AMD chlubili při ohlášení své mikroarchitektury Zen. Ale co tím vlastně myslí je otázka, na kterou nikdo nezná přesněou odpověď. Nejspíš nejde o nijak komplikované neuronosítě, ale o perceptrony, jak je popsáno v "Dynamic Branch Prediction with Perceptrons":[n]. Predikce musí být rychlá, ideálně dostupná v jednom taktu, navíc je vždy tlak na to, aby extra hardware zabíral co nejméně místa čipu a konzumoval co nejméně energie. x86 procesory mají několik nezávislých mechanismů pro predikci skoků. Jednak je již zmíněný to *branch predictor*, který odhaduje, zdaji bude skok proveden nebo ne a dále jsou to mechanismy odhadující, kam povede nepřímý skok. Jde jednak o "*branch target buffer* (BTB)":[btb] pro obecné skoky a pak *return stack buffer* (RSB) pro predikci cílů `ret` instrukcí. Protože `ret` se typicky vrátí na místo `call` instrukce, je RSB implementován jako jednoduchý zásobník. `call` na vrchol zásobníku vrazí svojí adresu a `ret` ji vyjme. Toho je možné využít pro "retpoline":[r] chránící před druhou variantou útoku Spectre. Nepřímý skok, který by se zkompiloval jako prosté /---code jmp *%r11 \--- se se správnými přepínači GCC/LLVM namísto toho zkompiluje jako /---code call set_up_target; capture_spec: // nekonečná smyčka slouží jako trampolína, kde uvázne spekulativní exekuce pause; jmp capture_spec; set_up_target: mov %r11, (%rsp); // tady se přepíše návratová adresa na zásobníku ret; // ret se vrátí na přepsanou adresu \--- Dojde náhradu za pár korespondujících `call`/`ret` instrukcí a na zásobníku se přepíše návratová adresa. To znemožní, aby byl spekulativně vykonán neznámý kód. Predikce `ret` použije adresu z RSB, která povede do nekonečné smyčky, a procesor poté, co odhalí, že jde o chybu, skočí nespekulativně na správnou adresu. Má to dopady na výkon, protože kód běží, jako kdyby byly spekulace a predikce zakázány. Relevantní čtení: - "Branch Prediction and the Performance of Interpreters - Don't Trust Folklore":[f] - branch prediction je natolik efektivní, že správně odhaduje závislé skoky v interpretech. - "Čím více se věci mění, tím více zůstávají stejné":[fc] - "KPTI/KAISER Meltdown Initial Performance Regressions":[http://www.brendangregg.com/blog/2018-02-09/kpti-kaiser-meltdown-performance.html] [sl]: https://deadbeef.k47.cz/files/meltdown.pdf [fbp]: http://funkcionalne.cz/2014/11/branch-prediction-modernich-procesoru/ [fbh]: http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani/ [fc]: http://funkcionalne.cz/2016/05/cim-vice-se-veci-meni-tim-vice-zustavaji-stejne/ [w]: https://en.wikipedia.org/wiki/Branch_predictor#Implementation [d]: https://danluu.com/branch-prediction/ [y]: http://web.eecs.umich.edu/~tnm/papers/yags.pdf [n]: https://www.cs.utexas.edu/~lin/papers/hpca01.pdf [btb]: https://en.wikipedia.org/wiki/Branch_target_predictor [r]: https://support.google.com/faqs/answer/7625886 [f]: https://hal.inria.fr/hal-01100647/document [b]: branch-prediction Poznámky k výkonu [poznamky] ===== 2017-12-28 # JVM, CPU Před nějakou dobou mi twitterem proletěla prezentace *Adventures in efficiency and performance* ("slajdy":[sli], "video":[vid]), kde autor mluvil o tom jak dosáhnout výkonu na JVM a vyprávěl o hardwaru. Některé informace mi přišly nedostatečné, tady jsou moje poznámky. V části *"Good" failed speculations* uvádí: > Both branches will be executed in parallel, only one of them will be retired Ukázkový kód vypadá takhle: /---code def foo(a: Array[Int], idx: Int) = { val s = a[idx] if (s < 0) idx + idx else idx - idx } \--- Záleží, jak bude kód zkompilován. Na jednu stranu může být převeden na compare+branch instrukce, které povedou k tomu, že CPU bude dynamicky spekulovat a bude záležet na branch prediction. Na druhou stranu to může vést k něčemu jkao tohle: /---code def foo(a: Array[Int], idx: Int) = { val s = a[idx] // val add = idx + idx // val sub = idx - idx // nezávislé operace, všechny tři můžou běžet najednou if (s < 0) add else sub // zkompilováno jako cmov instrukce bez skoku } \--- Poslední `if` nepředstavuje skok, ale instrukci `cmov` ("conditional move":[cmov]), která na základě porovnání přesune buď `add` nebo `sub`. Nejde o větvení programu a tedy ani nehrozí žádná penalizace při špatném odhadu skoku (*branch mispredict*). Nevím přesně, co myslel těmi *good failed speculation*. Současné x86 procesory (pokud se nemýlím) se nesnaží spekulativně provést obě větve podmínky a *retire* jen tu správnou. Další příklad, který je mnohem komplikovanější než je prezentováno: /---code val a: Array[Array[Int]] = ... val i = 0 while (i < n) { val j = 0 while (j < n) { if (fast) { a[i][j] += a[i][j] } else { // slow a[j][i] += a[j][i] } j += 1 } i += 1 } \--- Autor tvrdí, že rychlá verze (větev `fast`) byla 3x rychlejší až do příchodu Intel Skylake procesorů, které pak uvedly nějakou blíže nespecifikovanou změnu, která to změnila. Pomalá větev může být rychlá, "záleží na řadě faktorů":[n] jako například jak velká jsou data a jak jsou uspořádaná v paměti. Jde o to, že rychlá verze přečte všechny elementy pole `a[i]` jeden za druhým než se posune na následující pole `a[i+1]`. Pomalá verze přečte první element ze všech polí a pak se posune na druhý element. Rychlá verze čte sekvenčně bloky paměti, druhá skáče z místa na místo. Pokud je dat málo a vejdou se do cache, není moc co řešit (ale rychlá verze je stále preferovaná, protože se dá snáze vektorizovat bez "scatter/gather instrukcí":[sg]). Rychlá verze funguje tak dobře, protože hardware počítá s tímto chováním. Data z paměti tahá po *cache line* blocích (64B na současných x86). Když se dotknu prvního intu v poli, dalších 16 jich už čeká v cache, připravené pro rychlé čtení. CPU navíc dopředu načítá data, která budou pravděpodobně potřeba (*prefetch*), takže sekvenční průchod je zcela ideální. Prefetcher ale nemusí načítat jen data, která leží těsně vedle, ale dokáže detekovat kroky konstantní délky. Pokud jsou vnitřní pole malá a alokovaná sekvenčně, prefetcher rozpozná vzdálenost k dalšímu požadovanému prvku (*stride*) a načte ho dopředu. (Je ale méně efektivní, protože pořád musí vytáhnout celou *cache line*, za které použiji jen první prvek a než se dostanu k jeho sousedu, může být už vyhozená z cache). /---code | -- a[0] ----- | -- a[1] ----- | -- a[2] ----- | -- a[3] ----- slow: ^ ^ ^ ^ |<-a[0].length->|<-a[0].length->|<-a[0].length->| fast: ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ \--- Pokud jsou vnitřní pole alokována různě na haldě, tak to také nemusí být problém. Prefetcher zvládne pracovat na několika místech paměti najednou (něco jako 64 různých stránek). Kdyby tedy vnější pole bylo menší než tento limit, prefetcher by stále pomohl. I když jsou vnitřní pole rozesetá všude na haldě, ani to nemusí být konec. Záleží totiž i na použitém garbabe collectoru. Některé GC dělají kompakci haldy tak, že přesouvají objekty v pořadí ve kterém na ně narazí při procházení referencí. V tomto případě GC může narazit na pole `a`, zkopíruje ho na jedno místo a pak z něj začne procházet všechny reference na vnitřní pole a jedno po druhém je kopírovat vedle sebe. To povede k obnovení dobré lokality jako na schématu o něco výše. Skylake mohl uvést větší cache, prefetch na více stránkách paměti, více paralelních přístupů do paměti, větší OOO okno. Vliv může mít také asociativita cache a *cache eviction policy*. Nebudu tady teď mluvit scatter/gather SIMD instrukcích a dalších věcech, protože by to zabralo příliš místa a tenhle článek je už tak dost dlouhý. Zjistit co je rychlejší a hlavně proč je jako vždycky komplikované. Relevantní čtení: - "Hořící křemík & násobení matic":[n] - "Mikrobenchmarky jsou těžké":[mb] - "Závislost je špatná (pro váš program i pro váš hardware)":[za] - "Von Neumannovy lži":[vn] - "Mýtus o O(1) paměti":[my] - "Za jak dlouho procesor vynásobí tisíc čísel":[ti] - "Intel 64 and IA-32 Architectures Optimization Reference Manual":[opt] - "Úvod do podivností moderního hardwaru, které vás budou budit ze spaní":[uv] [h]: hugetables [sli]: https://d-d.me/talks/scalaworld2017/ [vid]: https://www.youtube.com/watch?v=8mFl8fywIP4 [opt]: https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-optimization-manual.html [n]: http://funkcionalne.cz/2017/06/horici-kremik-nasobeni-matic/ [mb]: http://funkcionalne.cz/2017/04/mikrobenchmarky-jsou-tezke/ [vn]: http://funkcionalne.cz/2017/05/von-neumannovy-lzi/ [my]: http://funkcionalne.cz/2016/09/mytus-o-o1-pameti/ [za]: http://funkcionalne.cz/2017/01/zavislost-je-spatna-pro-vas-program-i-pro-vas-hardware/ [ti]: http://funkcionalne.cz/2015/05/za-jak-dlouho-procesor-vynasobi-tisic-cisel/ [uv]: http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani/ [cmov]: https://en.wikipedia.org/wiki/Predication_(computer_architecture) [sg]: https://en.wikipedia.org/wiki/Gather-scatter_(vector_addressing) Java, Scala a regulární výrazy #4 - líné regexy a StackOverflowError [regex-4] ===== 2017-12-19 # Java, Scala, regex Jednou z nepříjemných vlastností regexů v Javě je to, že můžou snadno skončit přetečením zásobníku a StackOverflowError. Regexy dělají backtracking, jsou implementovány rekurzivně a proto jde o nevyhnutelnou eventualitu. Nejběžnější druhy opakování `.*`, `.+` a `.{m,n}` jsou *greedy* (lakomé, hladové nebo jak je to česky) a snaží se pojmout co nejdelší vstup. Když následující část regexu selže, začnou s backtrackingem a postupně upouštějí z požraného vstupu, dokud následující část uspěje. Aby to fungovalo, potřebují si pamatovat kam skočit zpátky a tato pozice je (často) uchována na zásobníku při rekurzivním volání. Když mám například regex `.+.{5}`, první část `.+` pojme celý vstup, pak zjistí, že následující regex `.{5}` pro pět libovolných znaků nepasuje, ustoupí o jeden znak, zkusí znovu, nepasuje, opět ustoupí a tak dále dokud nepojme všechno kromě pěti posledních znaků. Teprve potom `.{5}` sedne a celý regex skončí úspěchem. Naproti tomu líné (anglicky *lazy* nebo také *reluctant*) opakování `.*?`, `.+?` a `.{m,n}?` se snaží pojmout jen absolutní minimum, ideálně nic. V případě nouze, kdy následující regex selže, pojme jedno další opakování a pak otestuje následující část regexu. V regexu `.+?.{5}` první část `.+?` nejprve nepožere nic, otestuje následující část, když ta selže, požere jedno opakování víc, zkusí znovu a tak dále. Hladová a líná opakování nezmění jestli regex selže nebo ne, může změnit jen jak bude vstupní string rozdělen mezi uzávorkované skupiny. Z výše popsaných důvodů vyplývá, že hladové regexy někdy můžou skončit výjimkou StackOverflowError, zatímco líné proběhnou úspěšně. Například toto /---code ("\uD83D\uDC09 "*100000).matches(".*") \--- skončí StackOverflowError. Vstupem je řetězec, ve kterém se sto tisíckrát opakuje "symbol draka":[d], který je hned následovaný mezerou. Naproti tomu varianty /---code ("\uD83D\uDC09 "*100000).matches(".*?") ("\uD83D\uDC09 "*100000).matches(".*+") \--- skončí úspěšně, protože se liší způsobem, jakým je regex prováděn. Java obsahuje optimalizaci, že když jedno opakování regexu pojme stejný počet znaků jako minulé opakování, nevolá se rekurzivně, ale ve smyčce. Symbol `\uD83D\uDC09` zabírá dva `char`y a je hned následovaný jedno`char`ovou mezerou. `.` tedy střídavě matchuje jeden a dva `char`y a proto vždy dělá rekurzivní volání a eventuálně přeteče zásobník ("kód ve třídě Curly":[c], metody `match0`, `match1`, `match2`). Líná varianta si nemusí pamatovat, kam skočit zpátky a proto je implementována smyčkou, která nevyčerpá zásobník a neselže. Tohle se týká jen případů, kdy se opakuje jednoduchý neuzávorkovaný vzor (jako třeba `.`, `\W`, `\S` atd.) a je to problém/řešení jen pro stringy obsahující několikabajtové znaky. Ale ty nejsou zas tak vzácné v pro texty s emoji. Něco jako `(a|b+)+?` se vykonává vždy rekurzivně. Vnitřní implementace regexů je komplikovaná a aktivně se snaží předejít mnoha případům katastrofického backtrackingu. Proto se někdy jednoduchý vzor provádí poměrně překvapivě. Co přetečení a katastrofickému backtrackingu předchází mnohem spolehlivěji jsou takzvané *posesivní regexy*, "ale o nich něco napíšu příště":[r]. [c]: https://github.com/dmlloyd/openjdk/blob/jdk9/jdk9/jdk/src/java.base/share/classes/java/util/regex/Pattern.java#L4339 [d]: http://www.fileformat.info/info/unicode/char/1f409/index.htm [r]: regex-5 Java, Scala a regulární výrazy #3 - rychlost [regex-3] ===== 2017-12-05 # Java, Scala, regex Jak je na tom výkon regulárních výrazů ve srovnání s ručně psanými parsery? Otestoval jsem to na parsování logů webserveru. Regulární výraz vypadá takhle: /---code """^(\S++) (\S++) (\S++) \[([^]]++)\] "((?:\\"|[^"])++)" (\d++) (\d++|-) "((?:\\"|[^"])*+)" "((?:\\"|[^"])*+)".*$""".r \--- "Kód ručně napsaného parseru má 55 řádků":[g] a zabral mi přibližně stejně dlouho jako odladění regexu. Obě varianty vyprodukují identické výsledky. Jako vstupní data jsem použil vzorek 1680000 řádků logu "k47.cz":[k]. Regex je zpracuje za 9.26 vteřiny. Ruční variantě to trvá 1.3 vteřiny a z toho asi 0.3 vteřiny zabere jen alokace pod-řetězců. Ve výsledku je regex asi 7x pomalejší než ručně psaný kód. [k]: https://k47.cz [g]: https://gist.github.com/kaja47/5252063daabdd19b69caac524d8b0e6c Java, Scala a regulární výrazy #2 - rychlost [regex-2] ===== 2017-11-27 # Java, Scala, regex Stará poučka říká, že v Javě bychom měli regulární výrazy vždy dopředu zkompilovat a pak je opakovaně používat, protože kompilování je časově náročné. "V Javě je na to volání":[p] `Patten.compile("^===+$")`. "Ve Scale je možné použít kompaktnější zápis":[r] za pomocí kouzel implicitních metod `"^===+$".r`. Ale jak pomalé to vlastně je? To se dá jednoduše zjistit. Vytáhl jsem JMH, napsal benchmark, který hledá jednoduchý vzor. Jednou vzor nekompiluje, podruhé používá předkompilovaný vzor a potřetí hledá regex jen když je to nezbytně nutné. /---code @State(Scope.Thread) class regex { var lines: Array[String] = _ val regex = """^===+$""".r @Setup def prepare() = { lines = io.Source.fromFile("export.txt").getLines.toArray } @Benchmark def notCompiled(): Int = lines.count(l => l.matches("^===+$")) @Benchmark def compiled(): Int = lines.count { case regex() => true case _ => false } @Benchmark def compiledWithCheck(): Int = lines.count { l => (l.length > 0 && l.charAt(0) == '=') && regex.findFirstMatchIn(l).nonEmpty } } \--- Jako testovací data jsem použil kompletní export zdrojových textů blogu "k47.cz":[k], které mají dohromady něco kolem 8 MB a 143000 řádek textu. Výsledky jsou následující: /---code Benchmark Mode Cnt Score Error Units notCompiled thrpt 6 28,936 ± 2,298 ops/s compiled thrpt 6 94,120 ± 1,195 ops/s compiledWithCheck thrpt 6 526,849 ± 96,101 ops/s \--- Kompilovaný regex je tři-a-něco-krát rychlejší než nekompilovaný. Ale ten zaostává za případem, kdy se vyhodnocování regexu úplně vyhnu. Přitom regex by měl selhat hned potom, co přečte první znak a tedy by neměl dělat víc práce, než explicitní kontrola. Ale očividně má nezanedbatelnou režii. Pokud regex nebude často prováděn, je rychlejší dělat něco jako: /---code // zkompilovaný regex val linkRegex = """(?x) " ([^"]+?) " : \[ ([^\]\n]+?) \]""".r val linkCheck = "\":[" // používat ho následovně if (txt.contains(linkCheck)) { linkRegex.replaceAllIn(txt, m => makeLink(m)) } \--- Narychlo zkontrolovat, jestli zdrojový řetězec obsahuje nějakou fixní část regexu a teprve potom nechat regex dělat těžkou práci. Ale jako v případě každé optimalizace je třeba profilovat a benchmarkovat. Tento přístup zrychlil renderování textu "asciiblogu":[a] o 75%. Relevantní čtení: - "Mikrobenchmarky jsou těžké":[m] [m]: http://funkcionalne.cz/2017/04/mikrobenchmarky-jsou-tezke/ [k]: https://k47.cz [a]: https://github.com/kaja47/asciiblog [p]: https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html [r]: https://www.scala-lang.org/api/current/scala/util/matching/Regex.html Java, Scala a regulární výrazy [regex-1] ===== 2017-11-26 # Java, Scala, regex Poslední dobou zjišťuji, že práce s regulárními výrazy v Javě a Scale je neuspokojivá, matoucí a API nikdy nenabízí přesně to, co bych chtěl. Scala má "krásně vypadající metodu":[rf] `Regex#replaceAllIn(String, Match => String)`. Na první pohled je všechno jasné: Každý výskyt regulárního výrazu v prvním argumentu je předán funkci `Match => String`. Objekt typu `Match` reprezentuje výskyt vzoru. Funkce ho vezme jako argument a vyprodukuje string, který ho nahradí. Co by na tom mohlo být komplikovaného, žeano? Jednoduché volání, které na první pohled vypadá jako identita, není bezpečné: /---code regex.replaceAllIn(str, (m: Match) => m.group(0)) \--- `m.group(0)` představuje celou shodu (ostatní indexy jsou uzávorkované výrazy v regexu, `m.group(1)` jsou první závorky, `m.group("named")` jsou závorky pojmenované `named`) . V čem je problém? V tom, že výsledek funkce se nepoužije verbatim pro nahrazení, ale je interpretován. Každý výskyt řetězců `$0` - `$9` je interpretován jako podvýraz a `\$` znamená literál `$`. Když vstupní data obsahují dolar následovaný číslem, skončí to přinejlepším výjimkou, přinejhorším to udělá něco nečekaného. A to není to, co by člověk čekal nebo chtěl. V dokumentaci je toto chování dobře popsané, ale nepůsobí to, že by to tak mělo být. Korektní verze by měla vypadat takhle: /---code regex.replaceAllIn(str, (m: Match) => Regex.quoteReplacement(m.group(0))) \--- Interpretace stringu je přežitek "starších API":[rp], které nenabízely nahrazení přes fukci: /---code regex.replaceAllIn(str, replace: String) \--- V tomto případě nemůžu specifikovat logiku, která vypočítá nahrazovací string a proto jsou použity zpětné reference jako `$0` - `$9`. Ty poskytují aspoň nějakou rudimentární flexibilitu. Problém je v tom, že výsledný string musí být escapován a *hned potom* je interpretován (pro `$` a `\$`), což je zbytečná práce, která stojí zbytečný čas.[[1]] Ocenil bych jasné API s metodami jako - `Regex#replaceAllByPattern` - `Regex#replaceAllByString` které jasně říkají, co dělají a uživatel si může vybrat přesně to chování, které potřebuje. --- 1) Dodatek: I když má `quoteReplacement` měřitelnou režii, je téměř zanedbatelná. "Implementace počítá s tím":[i], že v naprosté většině případů nebude třeba nic escapovat. [rf]: https://www.scala-lang.org/files/archive/nightly/2.12.x/api/2.12.x/scala/util/matching/Regex.html#replaceAllIn(target:CharSequence,replacer:scala.util.matching.Regex.Match=%3EString):String [rp]: https://www.scala-lang.org/files/archive/nightly/2.12.x/api/2.12.x/scala/util/matching/Regex.html#replaceAllIn(target:CharSequence,replacement:String):String [i]: http://hg.openjdk.java.net/jdk10/master/file/be620a591379/src/java.base/share/classes/java/util/regex/Matcher.java#l809 Java - zostřování obrázků ====== 2017-11-21 # Java V Javě se dají obrázky jednoduše zostřit pomocí tříd "Kernel":[k] a "ConvolveOp":[c]. /---code def sharpen(src: BufferedImage): BufferedImage = { val kernel = new Kernel(3, 3, Array(-1f, -1f, -1f, -1f, 9f, -1f, -1f, -1f, -1f)) val op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null) op.filter(src, null) } \--- Klasický příklad matice pro zostření, která je zmíněna snad v každém tutoriálu: /---code -1 -1 -1 -1 9 -1 -1 -1 -1 \--- Znamená to znásobit hodnotu pixelu 9x a odečíst od něj hodnoty všech sousedních. To zostří obrázek, ale efekt je příliš silný. Když chci jemnějí detaily, je potřeba jiná matice. Důležité je, aby její součet byl 1. Takováhle matice povede k delikátnějšímu zostření: /---code -0.1 -0.1 -0.1 -0.1 1.8 -0.1 -0.1 -0.1 -0.1 \--- Funkce, která dělá zostředí volitelné intenzity, může vypadat takhle: /---code def sharpen(src: BufferedImage, strength: Float): BufferedImage = { require(strength > 0 && strength < 1) val arr = Array.fill[Float](9)(-strength) arr(4) = 8 * strength + 1 val kernel = new Kernel(3, 3, arr) val op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null) op.filter(src, null) } \--- [k]: https://docs.oracle.com/javase/8/docs/api/index.html?java/awt/image/Kernel.html [c]: https://docs.oracle.com/javase/8/docs/api/index.html?java/awt/image/ConvolveOp.html Koherence cache a atomické operace [koherence-cache-atomicke-ops] ===== 2017-10-07 # CPU Pokud vás zajímají nízkoúrovňové detaily procesorů a konkrétně jak multi-procesorový systém udržuje koherentní pohled na celou paměť prostřednictvím MESI protokolu, budou vás zajímat tyhle dva články: - "Cache coherency primer":[https://fgiesen.wordpress.com/2014/07/07/cache-coherency/] - "Atomic operations and contention":[https://fgiesen.wordpress.com/2014/08/18/atomics-and-contention/] Jestli vám to nestačí a chcete se dozvědět víc (ideálně *všechno*), doporučuji tuto bichli: - "A Primer on Memory Consistency and Cache Coherence":[b] Relevantní čtení: - "Memory Barriers: a Hardware View for Software Hackers":[p] - "Diving Deeper into Cache Coherency":[dd] - "Memory Barriers/Fences":[f] - "Cache Coherence - Onur Mutlu":[v2] - "Computer Architecture - Cache Coherence Protocols":[v1] - "Memory Coherence in Shared Virtual Memory Systems":[sh] (Tohle je zajímavý článek. Píše o tom, jak postavit systém s koherentní sílenou pamětí z několika strojů. Používá klasický princip *one more level of indirection*). [b]: https://class.stanford.edu/c4x/Engineering/CS316/asset/A_Primer_on_Memory_Consistency_and_Coherence.pdf [p]: http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf [f]: https://mechanical-sympathy.blogspot.cz/2011/07/memory-barriersfences.html [dd]: http://psy-lob-saw.blogspot.cz/2013/09/diving-deeper-into-cache-coherency.html [sh]: http://www.cs.virginia.edu/~zaher/classes/CS656/li.pdf [v2]: https://www.youtube.com/watch?v=flbg-MKkwmk [v1]: https://www.youtube.com/watch?v=L5FtGNMGyRM Nejrychlejší hashmapa pod sluncem [robin-hood-hashmap] ===== 2017-10-05 # DS Článek "I Wrote The Fastest Hashtable":[hash] popisuje, jak může vypadat nejrychlejší hashmapa pod sluncem, která poráží všechny vysoce optimalizované konkurenty. Interně nepoužívá běžné *open addressing*, *linear probing* ani "cucko hashing":[ch], ale o něco méně známou techniku Robin Hood hashování. Ta se nesnaží snížit množství kolizí ale průměrnou délku kolizního řetězce. Odtud pochází onen Robin Hood v názvu: bohatým bere (krátký řetěz kolizí) a chudým dává (dlouhý řetěz kolizí). To efektivně vede k `log(n)` nejhoršímu času přístupu a možnosti naplnit tabulku velice blízko 100% V lineárním *probingu* je běžné, že dlouhé kolizní řetězce rostou nejrychleji. To dává smysl - zabírají hodně slotů a proto je velká šance, že do nich spadnou další klíče a délka řetězce dál naroste. Robin Hood při vložení efektivně provádí insertion sort, který řadí podle počtu slotů o kolik je klíč vzdálený od své ideální pozice. Krátké kolizní řetězce mají také výhodu, že kdy žádaný klíč bude blízko, může se nacházet na stejné cache line jako první zkontrolovaný slot a proto není třeba načítat další data z RAM. Relevantní čtení: - "Kolize hashů pro mírně pokročilé":[kol] - "Robin Hood Hashing should be your default Hash Table implementation ":[robin1] - "More numerical experiments in hashing: a conclusion":[robin2] [hash]: https://probablydance.com/2017/02/26/i-wrote-the-fastest-hashtable/ [kol]: http://funkcionalne.cz/2016/02/kolize-hashu-pro-mirne-pokrocile/ [ch]: https://en.wikipedia.org/wiki/Cuckoo_hashing [robin1]: http://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation/ [robin2]: http://www.pvk.ca/Blog/more_numerical_experiments_in_hashing.html Dekódování x86 instrukcí [x86-decode] ===== 2017-09-23 # CPU x86 je barokní architektura, která na sebe za čtyři dekády existence nabalila spoustu bahna. Jde o jedinou z mála "přežívajících CISC ISA":[risc] a má všechno, co od se od takové sady očekává: Mraky instrukcí proměnné délky, různé adresovací módy a zpětnou kompatibilitu s relikty minulosti. Ale "kolik instrukcí moderní x86 se všemi rozšířeními vlastně má":[how]? Tuto otázku překvapivě není jednoduché zodpovědět. Záleží jak člověk počítá. Počet položek v Intelích manuálech, počet mnemonických zkratek, počet forem? Může jich být něco mezi 1503 a 6000 podle toho, co je považováno za unikátní instrukci. A teď jak tohle dokáže procesor dekódovat? x86 kraluje severům, desktopům a laptopům a nejde jen o důsledek prehistorického nasazení v IBM PC. Moderní x86 se umí hýbat a když všechno jede jak má, dokážou každý takt vykonat 4-5 instrukcí. To znamená načíst z paměti/cache, dekódovat, provést a zapsat výsledek až pěti operací proměnné délky z tisíců různých forem. Nejjednodušší je pochopitelně podvádět a nic nedekódovat. Proto procesory nesou L0 cache určenou pro instrukce již dekódované na interní mikro-operace. Moderní x86 jádra, ale i přesto dokážou rychle dekódovat proud čerstvých instrukcí. Klíčem je paralelní *pre-decode*, který nejdřív zjistí jen délky instrukcí. Příklad: Jádro v každém taktu natáhne 16B kódu, *pre-decode* pro každý bajt z tohoto bloku zjistí, jak by byla instrukce dlouhá, kdyby začínala na této pozici. To se dá udělat paralelně, protože není třeba brát zřetel na závislosti. x86 navíc není zas tak barokní, vypadá na první pohled a má určitou regularitu, kdy velké skupiny instrukcí mají stejnou délku. To značně zjednodušuje návrh *pre-decode* fáze. Délky instrukcí se spočítají za jeden takt a pošlou se do další fáze pipeline. Tam je už třeba brát v potaz sekvenci instrukcí. Když má první instrukce délku 3 bajty, dekodér přeskočí 3 bajty (a tedy všechny délky, který *pre-deceode* spekulativně spočítal). Teď je jasné, že na pozici +3B začíná validní instrukce, délka je už spočítaná a dekodér jen zjistí, do jaké funkční jednotky onu operaci poslat. V procesoru není velký switch s ramenem pro každou podobu každé instrukce, ale tato znalost je rozložena napříč čipem. Relevantní čtení: - "Breaking the x86 ISA":[isa] (prezentace o pokusech zjistit nezdokumentované instrukce na x86 procesorech) - "What's the definition of RISC?":[risc] - "Za jak dlouho procesor vynásobí tisíc čísel":[f1] - "Hyper-threading aneb Jak sakra může běžet víc vláken na jednom jádře?":[f2] - "L1I cache a iTLB – když ani spekulace nepomůžou":[f3] - "Hardware is the new software":[hwsw] ("paper":[hwswp]) [how]: https://fgiesen.wordpress.com/2016/08/25/how-many-x86-instructions-are-there/ [isa]: https://www.blackhat.com/docs/us-17/thursday/us-17-Domas-Breaking-The-x86-ISA.pdf [hwsw]: https://blog.acolyer.org/2017/06/19/hardware-is-the-new-software/ [hwswp]: https://www.microsoft.com/en-us/research/wp-content/uploads/2017/05/baumann-hotos17.pdf [risc]: https://danluu.com/risc-definition/ [f1]: http://funkcionalne.cz/2015/05/za-jak-dlouho-procesor-vynasobi-tisic-cisel/ [f2]: http://funkcionalne.cz/2015/01/hyper-threading-aneb-jak-sakra-muze-bezet-vic-vlaken-na-jednom-jadre/ [f3]: http://funkcionalne.cz/2015/05/l1i-cache-a-itlb-kdyz-ani-spekulace-nepomuzou/ Energetická efektivita programovacích jazyků [energeticka-efektivita] ===== rel: datacentrum-z-mobilu 2017-09-21 Software má dopad na svět. Pozitivní i negativní. Tím negativním je spotřeba energie, která někde musí být vyrobená a většinou je to z fosilních paliv. V roce 2015 byl Evropský průměr kolem 0.35kg CO2 na vyrobenou kilowatthodinu. Jeden server s 1000W zdrojem tedy za rok přispěje ke 3 tunám CO2 v atmosféře. To je něco, co bychom stále měli mít na srdci. Neotřelým nápadem bylo například "postavit datacentrum ze zrecyklovaných mobilů":[d], o kterém jsem se tu už zmiňoval. Mobilní procesory mají dobrý poměr výpočetní výkon per watt a navíc není třeba vyrábět nový hardware. Otázkou efektivity se také zabírá paper "Energy Efficiency across Programming Languages":[p]. Výsledek je celkem nepřekvapivý: Rychlejší jazyky, tedy ty ve kterých je možné napsat řešení, která vypočítá výsledek co nejrychleji, spotřebují méně energie. Jde o klasické kandidáty C, C++ a také mnohem novější "Rust":[r]. Java se za nimi drží v závěsu. I když vztah mezi časem a energií není zcela lineární, kompilované jazyky jasně vedou v absolutní spotřebě joulů. Měření byla provedena na drobných benchmarcích a nikoli velkých produkčních aplikacích. Je tedy třeba brát čísla s rezervou, jak to zmiňoval "Cliff Click zmiňoval ve svém podcastu":[ccp], když mluvil o "srovnání Javy s C++":[cccc]. Například aplikace napsaná v PHP nemusí nutně spolykat 28x víc šťávy než její C protějšek, když většinu času stráví čekáním na databázi, která je napsaná ve vysoce optimalizovaném C++. Podobně tomu je s regulárními výrazy, kde i neefektivní interpretované jazyky používají rychlé regex jádro. Když už tu píšu o efektivitě, měl bych se taky zmínit o jejím pravém opaku - o bitcoinu. Ten je největším provinilcem na poli zbytečně propálené energie. Dnes celá síť spolyká víc energie než dokáže vyprodukovat nukleární reaktor a předpokládá se, že v roce to bude víc než celé Dánsko. To je děsivé číslo. Naštěstí se začínají objevovat "návrhy jak bitcoinu podobnou síť použít ke smysluplné práci":[bc]. [p]: http://greenlab.di.uminho.pt/wp-content/uploads/2017/09/paperSLE.pdf [ccp]: cliff-click-podcast [bc]: https://blog.acolyer.org/2017/09/06/rem-resource-efficient-mining-for-blockchains/ [d]: datacentrum-z-mobilu [cccc]: http://www.cliffc.org/blog/2017/09/18/java-vs-cc-the-podcast/ [r]: rust-a-parsery Programming and Performance podcast [cliff-click-podcast] ===== 2017-09-19 "Cliff Click":[ccb] začal s podcastem *Programming and Performance*. Zatím jsou k dispozici dvě epizody: "Of Bugs and Coding Styles":[ccbc] a "Java vs C/C++":[cccc], a nebojím se, že do budoucna bude mít nouzi o zajímavá témata. Přece jenom je to *ten* Cliff Click, který přivedl JIT v JVM na světlo světa a pracoval v Azulu, vyvíjející vlastní procesory speciálně určené pro Java aplikace. Z jeho minulých přednášek mi v paměti utkvěly například: "Java on a 1000 Cores - Tales of Hardware / Software CoDesign":[j1000], "A Crash Course in Modern Hardware":[ccmh] nebo "A JVM Does That?":[ajdt]. [ccb]: http://www.cliffc.org/blog/ [cccc]: http://www.cliffc.org/blog/2017/09/18/java-vs-cc-the-podcast/ [ccbc]: http://www.cliffc.org/blog/2017/09/16/of-bugs-and-coding-styles/ [j1000]: https://www.youtube.com/watch?v=5uljtqyBLxI [ajdt]: https://www.youtube.com/watch?v=uL2D3qzHtqY [ccmh]: https://www.infoq.com/presentations/click-crash-course-modern-hardware B-stromy, persistence, copy-on-write a Btrfs [btrfs-paper] ===== 2017-09-17 # FS "B-trees, Shadowing, and Clones":[bt], Ohad Rodeh Tento paper položil základy Btrfs - *copy-on-write* souborového systému (FS), který podporuje efektivní vytváření snapshotů a klonů. Jako u mnoha jiných FS jsou i tady B-stromy použité jako klíčová datová struktura. Jediná odlišnost oproti běžným návrhům je fakt, že se změny neprovádí na místě a nedochází k přepisování existujících dat. Lidi z funkcionálních kruhů tohle dobře znají. Jde o obyčejné "plně persistentní stromy":[p] založené na kopírování cesty od listu ke kořeni. V hantýrce FS podivínů se této technice říká *shadowing*, ale jde o stejný princip - při modifikaci B-stromu (používají se B+stromy, které mají v listech ukazatele na datové stránky), je vytvořena nová cesta od listu ke kořeni. Odtud pochází ono *copy-on-write* - při zápisu se vždy kopíruje, nikdy se nepřemazávají stará data, ale je vytvořena nová kopie. Staré verze existují na disku dokud na ně něco odkazuje a pak jsou odstraněny garbage collectorem. Logicky z těchto starých persistentních verzích je možné vytvářet *snapshoty* a klony, tedy editovatelné historické větve souborového systému, které s tím původním sdílí maximum možných dat. To je opět klasické strukturální sdílení (*structural sharing*), které každý FP akolyta zná jako vlastní boty. Ve světě souborových systémů je třeba se vypořádat s explicitní alokací a uvolňováním smazaných bloků. Protože navrhovaný FS může sdílet datové bloky a strukturu B-stromů mezi různými klony, je nutné pro každý z nich udržovat čítač referencí, který udává kolik pointerů odkazuje na ten který blok. Čítače referencí jsou také udržovány v *copy-on-write* B-stromech. (CoW mechanismus je, jak je vidět, důležitý pro odolnost systému proti pádu.) Čítače referencí umožňují levné klonování - stačí jen zkopírovat kořen stromu a aktualizovat refcount všech potomků (nově na ně ukazuje o jeden rodič více). Všechny další změny se provádějí líně. Z odkazovaného článku vychází Btrfs, ale ten je - jako funkční systém a nejen prototyp - mnohem "komplikovanější":[b]. Relevantní čtení: - "Persistentní datové struktury":[p] - "Střeva databází":[s] [p]: http://funkcionalne.cz/2016/08/persistentni-datove-struktury/ [bt]: https://liw.fi/larch/ohad-btrees-shadowing-clones.pdf [b]: https://btrfs.wiki.kernel.org/index.php/Main_Page#Developer_documentation [s]: http://funkcionalne.cz/2016/01/streva-databazi/ Rust a parsery [rust-a-parsery] ===== 2017-09-15 Možná bych měl dát Rustu druhou šanci. Když jsem ho "před nějakou dobou zkoušel":[d], něco mi na něm nesedělo a místo toho jsem začal experimentovat s D. Poslední dobou ale stále častěji slyším pozitivní ohlasy na Rust a jeho slibný systém vlastnictví objektů. Zajímavý příklad silných stránek jazyka je "Nom, a byte oriented, streaming, zero copy, parser combinators library in Rust:[nom]. Jak název napovídá, jde o efektivní knihovnu *parser kombinátorů* napsanou v Rustu. Na jednu stranu poskytuje vysokoúrovňové a skoro-deklarativní API *parser kombinátorů* (parser vzniká kompozicí menších parserů za pomocí různých kombinátorů), na druhou stranu nabízí nativní rychlost (Rust kompilátor používá "LLVM jako back end":[ok]), nekopíruje paměť a přitom je bezpečný. Rust nepovolí získat pointer do neplatného bufferu, všechno pohlídá systém vlastnictví objektů. Relevantní čtení: - "Writing parsers like it is 2017":[pars] - "System programming in Rust: beyond safety":[sys] [d]: http://funkcionalne.cz/2017/06/jazyk-d-a-radix-sort/ [nom]: http://spw15.langsec.org/papers/couprie-nom.pdf [pars]: https://blog.acolyer.org/2017/08/15/writing-parsers-like-it-is-2017/ [sys]: https://blog.acolyer.org/2017/06/14/system-programming-in-rust-beyond-safety/ [ok]: optimalizujici-kompilator Velké stránky paměti [hugetables] ===== 2017-09-13 # CPU "Transparent Hugepages: measuring the performance impact":[t] Praktická demonstrace toho jaký dopad může mít povolení velkých stránek paměti. Mapování z virtuální na fyzickou adresu je spravováno na úrovni stránek. Ty na x86 můžou mít několik velikostí: 4kB, 2MB nebo 1GB. Operační systém udržuje toto mapování v paměti jako stromy (přesněji řečeno *trie*). Aby procesor nemusel při každém přístupu do paměti procházet tímto stromem, který je taky v paměti (nebo v lepším případě v cache), obsahuje malou cache nazývanou TLB (*translation lookaside buffer*) pro mapování několika naposledy použitých stránek. To vede ke stavu, že pokud chci číst ze stránky, která je v TLB, je to rychlé, ale pokud chci číst z té, která tam není, OS musí provést *page table walk*, najít příslušné mapování, přeložit adresu a to celou operaci značně zpomalí. Velké stránky byly zavedeny z pochopitelných důvodů: Je jimi možné pokrýt větší část virtuální paměti a snížit tak počet drahých *TLB miss*. Odkazovaný článek uvádí případ Java aplikace s 200GB haldou, na které povolení velkých stránek vede k nezanedbatelnému zrychlení. Program před změnou trávil 10% taktů jen překladem virtuální adresy na fyzickou a *page table walk* namísto užitečné práce. Relevantní čtení: - "L1I cache a iTLB - když ani spekulace nepomůžou":[itlb] - "JVM Anatomy Park #2: Transparent Huge Pages":[jap2] (ze série "JVM Anatomy Park":[jvm-anatomy-park]) - "How Bad Can 1GB Pages Be?":[bad] (někdy můžou velké stránky uškodit, všechno závisí na kontextu) [t]: https://alexandrnikitin.github.io/blog/transparent-hugepages-measuring-the-performance-impact/ [itlb]: http://funkcionalne.cz/2015/05/l1i-cache-a-itlb-kdyz-ani-spekulace-nepomuzou/ [jap]: jvm-anatomy-park [jap2]: https://shipilev.net/jvm-anatomy-park/2-transparent-huge-pages/ [bad]: http://www.pvk.ca/Blog/2014/02/18/how-bad-can-1gb-pages-be/ Deanonymizace agregovaných dat [deanonymizace] ===== 2017-09-11 # algo "Trajectory recovery from Ash: User privacy is NOT preserved in aggregated mobility data":[t] Tohle je něco podle mého gusta. Článek ukazuje, že je možné z agregovaných pozičních dat zpětně zrekonstruovat trajektorie jednotlivých lidí. Vychází ze dvou předpokladů: - lidé jsou v systému po dlouhou dobu (dvě sousední epochy obsahují agregované pozice víceméně stejných uživatelů) - lidé se pohybují předvídatelně Na těchto tezích je možné vybudovat systém, který zrekonstruuje jednotlivé trajektorie s poměrně velkou přesností. Když například mobilní operátor prodá agregovaná data, třetí strana z nich může vytěžit denní trajektorie uživatelů. To jí samo o sobě poskytne informaci, kde jednotliví lidé pracují nebo kde bydlí. Můžou zajít dál a přidat další informace, například místa plateb a spojit tuto pseudonymní trajektorii s konkrétní identitou. Zajímavý je také fakt, že snížení rozlišení vede ke zvýšení přesnosti rekonstrukce trajektorií. Relevantní čtení: - "Leave your phone at the door: side channels that reveal factory floor secrets":[p] [t]: https://blog.acolyer.org/2017/05/15/trajectory-recovery-from-ash-user-privacy-is-not-preserved-in-aggregated-mobility-data/ [p]: https://blog.acolyer.org/2016/11/09/leave-your-phone-at-the-door-side-channels-that-reveal-factory-floor-secrets/ Optimalizující kompilátor [optimalizujici-kompilator] ===== 2017-09-09 "Understanding Compiler Optimization - Chandler Carruth - Opening Keynote Meeting C++ 2015":[v] Přednáška, která představuje fungování optimalizujícího kompilátoru. Má tři hlavní fáze: - úklid po frontendu kompilátoru, (nedělá žádné optimalizace, jen vygeneruje LLVM IR v "SSA":[ssa] formě) - kanonizace IR (rozdílný kód, který je funkčně shodný vede k identickému IR) - *collapse abstractions* Tři klíčové abstrakce se kterými se potýká: - functions, calls, and the call graph - memory, loads and stores - loops [v]: https://www.youtube.com/watch?v=FnGCDLhaxKU [ssa]: https://en.wikipedia.org/wiki/Static_single_assignment_form Identifikace zašifrovaných videí [video] ===== 2017-09-07 "Beauty and the Burst: Remote Identification of Encrypted Video Streams":[https://beautyburst.github.io/beautyburst.pdf] Myšlenka je jednoduchá: Online video je přenášené jako série video fragmentů, jejichž velikost závisí na informačním obsahu dané scény. Série velikostí představuje unikátní otisk pro každé video. Třetí strana, která pozoruje velikosti paketů může zjistit, jaké video uživatel sleduje, a to i když je provoz plně zašifrovaný. Pouhé velikosti zasílaných bloků stačí k velice přesné identifikaci. ISP tak například může zjistit co jednotliví uživatelé sledují. Heterogenní persistentní kolekce [hhamt] ===== 2017-09-05 # DS, Funkcionální programování "Efficient Immutable Collections":[t], Michael Steindorfer Tohle je něco podle mého gusta, *tour de force* diplomová práce (*thesis*, co to je vlastně česky?), která zkoumá všechny možné postupy, jak implementovat paměťově efektivní kolekce v prostředí JVM. A když říkám všechny, myslím skutečně všechny. Autor Michael Steindorfer se nezalekne žádného problému a nezastaví se před ničím. --- Začalo to "Bagwellem inspirovanými":[bag] HAMT - *hash array mapped tries* - efektivní metodou jak reprezentovat persistentí funkcionální hashmapy jako velice široké a mělké stromy, které našly uplatnění ve standardní knihovně Scaly a Clojure. Ale u toho to neskončilo a Steindorfer začal postupně prezentovat sérii zlepšení. První z nich byl CHAMP ("Optimizing Hash-Array Mapped Tries for Fast and Lean Immutable JVM Collections":[champ]), který drobnou změnou interní struktury snížil spotřebu paměti, rychlost iterace a kontrolu rovnosti dvou kolekcí. Jednou ze změn je kanonizace, kdy kolekce obsahující stejná data mají vždy stejnou reprezentaci. To obecně neplatí, když přidávám a mažu prvky. Pokud je kolekce vždy kanonická, jsem si jistý, že když se liší jejich struktura, jde o dvě rozdílné kolekce a nemusím kontrolovat nic dalšího. Pokud je struktura stejná, kolekce můžou být stejné, ale abych to potvrdil, musím je rekurzivně porovnávat do hloubky. Autor se pak začal zabývat specializací ("Code Specialization for Memory Efficient Hash Tries":[spec]), tedy generováním Java tříd, které neobsahují odkaz na pole (jak je běžné v implementaci HAMT), ale heterogenní pole klíčů/hodnot a podstromů (respektive specializovaných primitivních hodnot) je inlinováno přímo do těla třídy jako její atributy. To značně sníží paměťové nároky, protože není třeba uchovávat hlavičku vnitřního pole, které je většinou velice malé. Problém spočívá v kombinatorické explozi počtu tříd. V naivním případě je potřeba vygenerovat jednu třídu pro každou možnou podobu struktury uzlu. Autor k tomu přistoupil pragmaticky. Většina stromů je malá, obsahuje nejčastěji dva nebo tři potomky a u těchto malých stromů specializace přinese největší redukci místa. Proto stačí vygenerovat specializace jen pro tyto případy a získat tak největší prospěch s nejmenšími náklady. Logickým vyústěním byla jeho PhD práce ("Efficient Immutable Collections":[t]), která kombinuje všechny do té doby publikované články a rozvíjí je o krok dál, směrem k efektivní reprezentaci multimap. Ukazuje se, že stejné techniky inline bitmap, které jsou použité v HAMT k rozlišení případů prázdný slot/klíč+hodnota/podstrom, lze rozšířit pro diskriminaci mezi libovolným množstvím heterogenních případů, tedy například mapování klíč->hodnota/klíč->množina hodnot a tak efektivně reprezentovat multimapy. Autor také krátce "prezentoval své výsledky":[vid]. Relevantní čtení: - "Persistentní datové struktury":[pers] - "Kolize hashů pro mírně pokročilé":[kol] [vid]: http://www.youtube.com/watch?v=D8Y294vHdqI [t]: https://michael.steindorfer.name/publications/phd-thesis-efficient-immutable-collections.pdf [champ]: http://michael.steindorfer.name/publications/oopsla15.pdf [spec]: http://michael.steindorfer.name/publications/gpce14.pdf [pers]: http://funkcionalne.cz/2016/08/persistentni-datove-struktury/ [kol]: http://funkcionalne.cz/2016/02/kolize-hashu-pro-mirne-pokrocile/ [bag]: http://infoscience.epfl.ch/record/64398?ln=en JVM Anatomy Park [jvm-anatomy-park] ===== 2017-09-03 # JVM, Java Velice zajímavá série krátkých článků "Alekseye Shipilёva":[tw] o vnitřnostech a vnitřním fungování JVM. - "JVM Anatomy Park #1: Lock Coarsening and Loops":[https://shipilev.net/jvm-anatomy-park/1/] - "JVM Anatomy Park #2: Transparent Huge Pages":[https://shipilev.net/jvm-anatomy-park/2-transparent-huge-pages/] - "JVM Anatomy Park #3: GC Design and Pauses":[https://shipilev.net/jvm-anatomy-park/3-gc-design-and-pauses/] - "JVM Anatomy Park #4: TLAB allocation":[https://shipilev.net/jvm-anatomy-park/4-tlab-allocation/] - "JVM Anatomy Park #5: TLABs and Heap Parsability":[https://shipilev.net/jvm-anatomy-park/5-tlabs-and-heap-parsability/] - "JVM Anatomy Park #6: New Object Stages":[https://shipilev.net/jvm-anatomy-park/6-new-object-stages/] - "JVM Anatomy Park #7: Initialization Costs":[https://shipilev.net/jvm-anatomy-park/7-initialization-costs/] - "JVM Anatomy Park #8: Local Variable Reachability":[https://shipilev.net/jvm-anatomy-park/8-local-var-reachability/] - "JVM Anatomy Park #9: JNI Critical and GC Locker":[https://shipilev.net/jvm-anatomy-park/9-jni-critical-gclocker/] - "JVM Anatomy Park #10: String.intern()":[https://shipilev.net/jvm-anatomy-park/10-string-intern/] - "JVM Anatomy Park #11: Moving GC and Locality":[https://shipilev.net/jvm-anatomy-park/11-moving-gc-locality/] - "JVM Anatomy Park #12: Native Memory Tracking":[https://shipilev.net/jvm-anatomy-park/12-native-memory-tracking/] Shipilёv také přednášel o "JVM benchmarkovacím nástroji JMH":[https://www.youtube.com/watch?v=VaWgOCDBxYw] Relevantní čtení: - "Jak JVM volá virtuální metody, jaká temná božstva musí vzývat, aby to bylo aspoň trochu rychlé":[virt] [tw]: https://twitter.com/shipilev/ [virt]: http://funkcionalne.cz/2015/09/jak-jvm-vola-virtualni-metody-jaka-temna-bozstva-musi-vzyvat-aby-to-bylo-aspon-trochu-rychle/ Datacentrum z mobilů [datacentrum-z-mobilu] ===== 2017-09-01 "Towards deploying decommissioned mobile devices as cheap energy-efficient compute nodes":[dp] Další zajímavost o které píše *The Daily Paper* - nápad postavit datacentrum z vyřazených mobilů namísto klasických serverových kudů křemíku. To má několik výhod, které mě zcela fascinovaly: - Moderní smartphony mají výkonné procesory a několik giga paměti. - Jejich poměr výkon/watt je velice příznivý. - Jde o vyřazené telefony, které jsou zrecyklované jako jádro datacentra. To je zelené řešení, které neprodukuje CO2 na výrobu nového hardware. - Mobily mají baterii. Není tedy potřeba UPS/distribuovanou UPS a telefony můžou ve špičkách konzumovat víc energie než na kolik je dimenzováno datacentrum. Když později běží nevytížené, baterie se dobije. Relevantní čtení: - "The Datacenter as a Computer: An Introduction to the Design of Warehouse-Scale Machines":[dc] - "Determining application-specific peak power and energy requirements for ultra-low power processors":[https://blog.acolyer.org/2017/05/23/determining-application-specific-peak-power-and-energy-requirements-for-ultra-low-power-processors/] [dp]: https://blog.acolyer.org/2017/08/25/towards-deploying-decommissioned-mobile-devices-as-cheap-energy-efficient-compute-nodes/ [dc]: http://www.morganclaypool.com/doi/abs/10.2200/S00193ED1V01Y200905CAC006 Propustnost pamětí [propustnost-pameti] ===== 2017-08-30 # CPU, paměť "Memory bandwidth":[https://fgiesen.wordpress.com/2017/04/11/memory-bandwidth/], Fabian Giesen Tenhle článek ukazuje jaká je relativní propustnost pamětí vzhledem k CPU a GPU. I když absolutní propustnost stoupá, s tou relativní to není tak slavné. - Prastarý MOS 6502 z roku 1975 má propustnost 4B / instrukci - Nové Core i7 má propustnost kolem 1B / instrukci (detaily v článku) - Pokud beru v potaz SIMD, má ono Core i7 propustnost 0.125B / skalární operaci v SIMD instrukci - Grafická karta GeForce GTX 1080Ti má propustnost 0.09B / skalární operaci v SIMD instrukci Jak je vidět, tak relativní propustnost pamětí klesá i přes všechny pokroky Moorova zákona. Grafické karty mají paměti s obrovskou absolutní propustností, ale také mají obrovské množství výpočetních jednotek (v případě GPU jde o velice široké SIMD procesory, ale to je detail). K tomu musíme přičíst, že absolutní latence DRAM klesla 4-5x od 80 let, ale takt procesorů se zvýšil o tři řády. To je důvod, proč jsou cache paměti na straně hardwaru a lokalita přístupu k paměti na straně softwaru tak důležité. Procesory jsou (většinou) dostatečně rychlé, RAM představuje úzké hrdlo. Relevantní čtení: - "Hořící křemík & násobení matic":[l1] (optimalizace násobení matic spočívá hlavně v lepším využití procesorové cache) - "Lokalita v grafech a negrafech":[l2] (lokalita je důležitá) - "Mýtus o O(1) paměti":[l3] - "Úvod do podivností moderního hardwaru, které vás budou budit ze spaní":[l4] - "What every programmer should know about memory":[l5] - "Why do CPUs have multiple cache levels?":[https://fgiesen.wordpress.com/2016/08/07/why-do-cpus-have-multiple-cache-levels/] [l1]: http://funkcionalne.cz/2017/06/horici-kremik-nasobeni-matic/ [l2]: http://funkcionalne.cz/2017/02/lokalita-v-grafech-a-negrafech/ [l3]: http://funkcionalne.cz/2016/09/mytus-o-o1-pameti/ [l4]: http://funkcionalne.cz/2016/07/uvod-do-podivnosti-moderniho-hardwaru-ktere-vas-budou-budit-ze-spani/ [l5]: http://funkcionalne.cz/2013/03/what-every-programmer-should-know-about-memory/ Branch prediction (implementace v procesorech) [branch-prediction] ===== 2017-08-28 # CPU Dan Luu napsal dobrý článek o tom, "jak funguje a jak je implementována *branch prediction*":[dl] v procesorech a proč je důležité, aby byla co nejpřesnější. V běžném kódu je nepodmíněný skok (*branch*) průměrně každou pátou instrukcí. V pipelinovaných procesorech je cíl skoku známý až v pozdějších fázích pipeline, ale procesor potřebuje znát cíl co nejdříve, aby mohl začít načítat a dekódovat instrukce, a v pipline nevznikla bublina. To řeší *branch predictor*, který odhaduje díl skoku na základě dřívějšího chování. Většina skoků se chová předvídatelně a velká část z nich je ve smyčkách. Když skočil stokrát, s největší pravděpodobností skočí i teď. *Branch miss* (tedy špatný odhad) znamená zahození celé pipeline, která obsahuje instrukce z nesprávné větve programu. Pro pipeline s hloubkou 20 to tedy znamená ztrátu ~20 taktů. Pokud je každá pátá instrukce skok a procesor by nedělal žádné odhady vedlo by to ke ~4x zpomalení. V článku jsou prezentovány *predictory* se vzrůstající sofistikovaností, které se snaží zachytit stéle komplikovanější a komplikovanější vzory a omezit patologické chování (BP je založen na hashování a proto se okrajově dotýká hashmap, bloom filterů, count-min skečů a "kolizí hashů":[kol]). Co největší přesnost je potřeba, protože (v prezentovaném modelu) i pouhých 5% nepřesnosti vede k poklesu výkonu o ~20%. Proto vývoj v tomto oboru neustává. Některé novější *predictory* jsou popsány například v: - "The YAGS Branch Prediction Scheme":[yags] (snaží se eliminovat kolize výčtem negativních případů, zároveň jsou tu vysvětleny další druhy *predictorů*) - "Dynamic Branch Prediction with Perceptrons":[perc] (BP založený na jednoduchých neuronových sítích, které se snaží naučit lineární funkci namísto historických tabulek) Ale to ani zdaleka není vyčerpávající seznam. Mnoho dalších metod a zlepšení bylo navrženo od té doby. Relevantní čtení: - "Branch prediction moderních procesorů":[bp] [dl]: https://danluu.com/branch-prediction/ [perc]: https://www.cs.utexas.edu/~lin/papers/hpca01.pdf [yags]: http://web.eecs.umich.edu/~tnm/papers/yags.pdf [bp]: http://funkcionalne.cz/2014/11/branch-prediction-modernich-procesoru/ [kol]: http://funkcionalne.cz/2016/02/kolize-hashu-pro-mirne-pokrocile/