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 Twit­teru jsem za­zna­me­nal vtip­nou etudu. Stalo se to už před ně­ja­kou dobou, ale tenhle článek mi ne­do­kon­čený ležel na disku. Nicméně začalo to tímhle twee­tem.

Podle všeho je v Ja­vaScriptu

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

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

if (obj) { return obj.x }

Je to proto, že ke kon­t­role proti undefined jsou po­třeba jen 2 x86 in­strukce (cmpjz). Na­proti tomu kon­t­rola prav­di­vosti obnáší kon­t­rolu jestli to není undefined, null, nula, prázdný string a nej­spíš ještě něco dal­šího.

To ale není za­jí­mavé. Sranda začala v oka­mžiku kdy DHH napsal mi­k­ro­ben­chmark, který podle něj pro­ká­zal, že zrych­lení není patrné. Velice rychle mu někdo začal dů­razně vy­svět­lo­vat, že jeho ben­chmark je k ničemu a nic ne­do­ka­zuje.

Zmí­něný ben­chmark vy­pa­dal ná­sle­dovně. 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 ta­ko­vé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 ben­chmark ben­chmar­kuje hlavně sám sebe a neměří nic uži­teč­ného. Rozdíl mezi noop funkcí a dvěma ostat­ními je jen mi­ni­mální. To zna­mená, že se v nich něco děje, ale režie ben­chmar­ko­va­cího ná­stroje je veliká a všechno utopí.

Mi­k­ro­ben­chmar­ko­vání je zrádné, zvlášť v pro­středí s agre­siv­ním JITem, jako jsou všechny Ja­vov­ské nebo JS vir­tu­ální stroje sto­jící za po­zor­nost. Je­di­ným po­slá­ním JITu je op­ti­ma­li­zo­vat a on je v tom za­tra­ceně dobrý. Když si člověk nedá pozor, JIT může kom­pletně od­stra­nit tes­to­vaný kód, pro­tože ten na­pří­klad nemá žádné ve­d­lejší efekty nebo nic ne­vrací. V testu můžu volat nějaké funkce, JIT je spe­ku­la­tivně in­li­nuje, zjistí, že kód je zby­tečný, pro­tože nedělá nic vi­di­tel­ného zvenčí a od­straní ho, tělo smyčky na­jed­nou může být prázdné, tak ji také od­straní a takto může po­kra­čo­vat dál, dokud kom­pletně ne­vy­hlodá měřený kód a ne­zů­sta­nou jen cha­padla ben­chmar­ko­va­cího ná­stroje, který měří svou vlastní režii.

Když člověk chce měřit výkon takhle šíleně ma­lič­kého kusu kódu – jde do­slova jen o hrstku in­strukcí – musí si dávat za­tra­ceně velký pozor a i přesto je třeba se po­dí­vat na de­kom­pi­lo­vaný as­sem­bler, pro­tože jinak si nikdy nemůže být jistý.

Pokud bych se cítil od­vážně, mohl bych z oněch tří čísel vy­de­du­ko­vat, ž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 zrych­lení o 30%, přesně podle vý­chozí teze. Ale jak říkám: Bůh ví, co se vlastně měří. Mi­k­ro­ben­chmar­ko­vání je zrádné a člověk nesmí mít slepou důvěru v po­u­žité ná­stroje a musí jim dobře ro­zu­mět, aby vý­sledná čísla byla k něčemu.


Dále k tématu:

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