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

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