funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

Mikrobenchmarky jsou těžké

17. 4. 2017 — k47

Na Twitteru jsem zaznamenal vtipnou etudu. Stalo se to už před nějakou dobou, ale tenhle článek mi nedokončený ležel na disku. Nicméně začalo to tímhle tweetem.

Podle všeho je v JavaScriptu

if (obj !== undefined) { return obj.x }

v průměru o 15% rychlejší než stručné

if (obj) { return obj.x }

Je to proto, že ke kontrole proti undefined jsou potřeba jen 2 x86 instrukce (cmp a jz). Naproti tomu kontrola pravdivosti obnáší kontrolu jestli to není undefined, null, nula, prázdný string a nejspíš ještě něco dalšího.

To ale není zajímavé. Sranda začala v okamžiku kdy DHH napsal mikrobenchmark, který podle něj prokázal, že zrychlení není patrné. Velice rychle mu někdo začal důrazně vysvětlovat, že jeho benchmark je k ničemu a nic nedokazuje.

Zmíněný benchmark vypadal následovně. Já jen přidal třetí test nihilist-friendly.

const Benchmark = require('benchmark')
const suite = new Benchmark.Suite

let a = undefined

suite
  .add('human-friendly',    function () { if (a) true })
  .add('machine-friendly',  function () { if (a !== undefined) true })
  .add('nihilist-friendly', function () {})
  .on('complete', function () { console.log('Fastest is ' + this.filter('fastest').map('name')) })
  .on('cycle', function(event) { console.log(String(event.target)) })
  .run()

Výsledky jsou takovéto:

human-friendly    x 103,629,158 ops/sec ±0.79% (93 runs sampled)
machine-friendly  x 105,345,660 ops/sec ±1.00% (90 runs sampled)
nihilist-friendly x 110,587,292 ops/sec ±1.07% (92 runs sampled)

Z čísel je vidět, že benchmark benchmarkuje hlavně sám sebe a neměří nic užitečného. Rozdíl mezi noop funkcí a dvěma ostatními je jen minimální. To znamená, že se v nich něco děje, ale režie benchmarkovacího nástroje je veliká a všechno utopí.

Mikrobenchmarkování je zrádné, zvlášť v prostředí s agresivním JITem, jako jsou všechny Javovské nebo JS virtuální stroje stojící za pozornost. Jediným posláním JITu je optimalizovat a on je v tom zatraceně dobrý. Když si člověk nedá pozor, JIT může kompletně odstranit testovaný kód, protože ten například nemá žádné vedlejší efekty nebo nic nevrací. V testu můžu volat nějaké funkce, JIT je spekulativně inlinuje, zjistí, že kód je zbytečný, protože nedělá nic viditelného zvenčí a odstraní ho, tělo smyčky najednou může být prázdné, tak ji také odstraní a takto může pokračovat dál, dokud kompletně nevyhlodá měřený kód a nezůstanou jen chapadla benchmarkovacího nástroje, který měří svou vlastní režii.

Když člověk chce měřit výkon takhle šíleně maličkého kusu kódu - jde doslova jen o hrstku instrukcí - musí si dávat zatraceně velký pozor a i přesto je třeba se podívat na dekompilovaný assembler, protože jinak si nikdy nemůže být jistý.

Pokud bych se cítil odvážně, mohl bych z oněch tří čísel vydedukovat, že cena noop funkce je 29 ns, cena rychlé funkce je 30.5 ns a pomalé 31.1 ns. Když odečtu těch 29 ns, je to 2.1 ns proti 1.5 ns, to je zrychlení o 30%, přesně podle výchozí teze. Ale jak říkám: Bůh ví, co se vlastně měří. Mikrobenchmarkování je zrádné a člověk nesmí mít slepou důvěru v použité nástroje a musí jim dobře rozumět, aby výsledná čísla byla k něčemu.


Dále k tématu:

@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články