Страницы

четверг, 16 марта 2017 г.

JavaScript Execution Optimization

Небольшой этюд на тему оптимизации в V8, или сказ о том как мономорфизм влечет за собой оптимизацию, а полиморфизм (не путаем с subtype или parametric polymorphism), в свою очередь, деоптимизацию JavaScript функций, с картинками. По мотивам публикаций Benedikt Meurer и Вячеслава Егорова. Для того, чтобы по-взрослому разобраться в том, как оптимизировать JavaScript код, рекомендую почитать публикации уважаемых разработчиков, упомянутых выше, а также документацию V8. Дисклеймер: "Premature optimization is the root of all evil", - Donald Knuth.


Для начала создадим несколько схожих по структуре объектов:
- test.js:
const a = { x: 1, y: 2, z: 3 };
const b = {};
b.x = 1;
b.y = 2;
b.z = 3;
const C = function ({ x = 1, y = 2, z = 3 }) {
  this.x = x;
  this.y = y;
  this.z = z;
};
const c = new C({});
const d = Object.assign({}, { x: 1, y: 2, z: 3 });
const e = Object.assign({}, a, { x: 5 });
const f = Object.assign({ x: 1, y: 2, z: 3 }, { y: 7, z: 9 });
const fn = ({ x = 1, y = 2, z = 3 }) => ({ x, y, z });
 
const inspect = require('util').inspect;
 
console.log(`
a: ${inspect(a)},
b: ${inspect(b)},
c: ${inspect(c)},
d: ${inspect(d)},
e: ${inspect(e)},
f: ${inspect(f)},
fn(b): ${inspect(fn(b))}`);


А теперь посмотрим что "думает" по поводу их "схожести" V8:
- test.js:
//...
const haveSameMap = (v1, v2) => %HaveSameMap(v1, v2);
 
console.log(`  
a and b have same map: ${haveSameMap(a, b)},
a and c have same map: ${haveSameMap(a, c)},
a and d have same map: ${haveSameMap(a, d)},
a and e have same map: ${haveSameMap(a, e)},
a and f have same map: ${haveSameMap(a, f)}
a and fn(b) have same map: ${haveSameMap(a, fn(b))}`);


Судя по всему оптимизирующий копмилятор V8 оптимизирует функцию, если в нее будут прилетать аргументы a или f, и деоптимизирует функцию, как только в нее полетят аргументы b, c, d или e (обращаю внимание на результат fn(b)).

Предлагаю проверить:
- test.js:
//...
const getOptimizationStatus = (fn) => {
  switch (%GetOptimizationStatus(fn)) {
    case 1: {
      return 'optimized';
    }
    case 2: {
      return 'not optimized';
    }
    case 3: {
      return 'always optimized';
    }
    case 4: {
      return 'never optimized';
    }
    case 6: {
      return 'maybe deoptimized';
    }
    case 7: {
      return 'optimized by TurboFan';
    }
    default: {
      return 'unknown';
    }
  }
};
 
const optimize = (fn) => %OptimizeFunctionOnNextCall(fn);
 
const fn1 = (v1, v2) => v1 === v2;
 
console.log(`
fn1: ${fn1}`);
fn1(a, a);
fn1(a, a);
console.log(`fn1(a, a) && status: ${getOptimizationStatus(fn1)}`);
optimize(fn1);
fn1(a, f);
console.log(`fn1(a, f) && status: ${getOptimizationStatus(fn1)}`);
fn1(a, b);
console.log(`fn1(a, b) && status: ${getOptimizationStatus(fn1)}`);


Попробуем отправить в функцию в качестве аргумента массив:
- test.js:
//...
const fn2 = (arr) => arr[0] === arr[1];
 
console.log(`
fn2: ${fn2}`);
fn2([a, a]);
fn2([a, a]);
console.log(`fn2([a, a]) && status: ${getOptimizationStatus(fn2)}`);
optimize(fn2);
fn2([a, f]);
console.log(`fn2([a, f]) && status: ${getOptimizationStatus(fn2)}`);
fn2([a, b]);
console.log(`fn2([a, b]) && status: ${getOptimizationStatus(fn2)}`);


Посмотрим как все пройдет, если использовать destructuring:
- test.js:
//...
const fn3 = ({ x, y, z }) => ({ x, y, z });
 
console.log(`
fn3: ${fn3}`);
fn3(a);
fn3(a);
console.log(`fn3(a) && status: ${getOptimizationStatus(fn3)}`);
optimize(fn3);
fn3(f);
console.log(`fn3(f) && status: ${getOptimizationStatus(fn3)}`);
fn3(b);
console.log(`fn3(b) && status: ${getOptimizationStatus(fn3)}`);
 
const fn4 = ([v1, v2]) => v1 === v2;
 
console.log(`
fn4: ${fn4}`);
fn4([a, a]);
fn4([a, a]);
console.log(`fn4([a, a]) && status: ${getOptimizationStatus(fn4)}`);
optimize(fn4);
fn4([a, f]);
console.log(`fn4([a, f]) && status: ${getOptimizationStatus(fn4)}`);
fn4([a, b]);
console.log(`fn4([a, b]) && status: ${getOptimizationStatus(fn4)}`);


Проверим что будет, если использовать rest:
- test.js:
//...
const fn5 = (...arr) => arr[0] === arr[1];
 
console.log(`
fn5: ${fn5}`);
fn5(a, a);
fn5(a, a);
console.log(`fn5(a, a) && status: ${getOptimizationStatus(fn5)}`);
optimize(fn5);
fn5(a, f);
console.log(`fn5(a, f) && status: ${getOptimizationStatus(fn5)}`);
fn5(a, b);
console.log(`fn5(a, b) && status: ${getOptimizationStatus(fn5)}`);


TurboFan in action.

Для теста я использовал Node.js версии 6.10.0:

В версии 7.7.3 результат будет отличаться из-за более новой версии V8:


Сравниваем картинки: destructuring массива теперь тоже оптимизируется.

Делаем выводы.

И еще пара ссылок по теме:
Optimization killers
V8 bailout reasons