Небольшой этюд на тему оптимизации в V8, или сказ о том как мономорфизм влечет за собой оптимизацию, а полиморфизм (не путаем с subtype или parametric polymorphism), в свою очередь, деоптимизацию JavaScript функций, с картинками. По мотивам публикаций Benedikt Meurer и Вячеслава Егорова. Для того, чтобы по-взрослому разобраться в том, как оптимизировать JavaScript код, рекомендую почитать публикации уважаемых разработчиков, упомянутых выше, а также документацию V8. Дисклеймер: "Premature optimization is the root of all evil", - Donald Knuth.
Для начала создадим несколько схожих по структуре объектов:
- test.js:
А теперь посмотрим что "думает" по поводу их "схожести" V8:
- test.js:
Судя по всему оптимизирующий копмилятор V8 оптимизирует функцию, если в нее будут прилетать аргументы a или f, и деоптимизирует функцию, как только в нее полетят аргументы b, c, d или e (обращаю внимание на результат fn(b)).
Предлагаю проверить:
- test.js:
Попробуем отправить в функцию в качестве аргумента массив:
- test.js:
Посмотрим как все пройдет, если использовать destructuring:
- test.js:
Проверим что будет, если использовать rest:
- test.js:
TurboFan in action.
Для теста я использовал Node.js версии 6.10.0:
В версии 7.7.3 результат будет отличаться из-за более новой версии V8:
Сравниваем картинки: destructuring массива теперь тоже оптимизируется.
Делаем выводы.
И еще пара ссылок по теме:
- Optimization killers
- V8 bailout reasons
Для начала создадим несколько схожих по структуре объектов:
- 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
Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации