DevGang
Авторизоваться

Может ли производительность JSON.parse() быть улучшена?

JSON.parse - это медленный способ создания копии объекта. Но можем ли мы на самом деле улучшить производительность нашего кода?

Проблема

Создание копии объекта является обычной практикой в ​​JS. Вы, вероятно, сделали это при создании редукторов в Redux или где-либо еще. В настоящее время наиболее используемый синтаксис для этого распространен:

const objA = { name: 'Jack', surname: 'Sparrow' };
const objB = { ...objA };

Использование на практике:

function dataReducer(state = { name: '', surname: '' }, action) {
  switch (action.type) {
    case ACTION_TYPES.SET_NAME:
      return {
        ...state,
        name: action.name,
      };
    case ACTION_TYPES.SET_SURNAME:
      return {
        ...state,
        surname: action.surname,
      };
    default:
      return state;
  }
}

Но это можно сделать разными способами (не считая разных библиотек)

const objC = Object.assign({}, objA);
const objD = JSON.parse(JSON.stringify(objA));

Если вы проверите, сколько времени потребуется, чтобы скопировать объект, используя эти методы, вы получите результаты, подобные 1⁰⁹ (каждый раз, когда мы копируем objA):

test with spread: 14 ms. 
test with Object.assign: 36 ms. 
test with JSON.parse: 702 ms.

Очевидно JSON.parse, самый медленный из них, и с некоторым отрывом. Почему вы даже должны рассмотреть возможность использования JSON.parse вместо распространения (...)?

Не очень очевидное поведение движка V8

Все сводится к тому, как V8 оптимизирует функции. Каждый раз, когда функция выполняется, V8 сравнивает переданный ей объект IC (Inline Cache), и если Shape объект хранится внутри одного из «кэшей», тогда V8 может следовать «быстрому пути».

Так что если у вас есть такая функция

function test(obj) {
  let result = '';
  for (let i = 0; i < N; i += 1) {
    result += obj.a + obj.b;
  }
  return result;
}

Вы можете запустить его с несколькими объектами одинаковой формы и иметь отличную производительность

const jack = { name: 'Jack', surname: 'Sparrow' };
const frodo = { name: 'Frodo', surname: 'Baggins' };
const charles = { name: 'Charles', surname: 'Xavier' };
test(jack);
test(frodo);
test(charles);

Причина в том, что V8 собирается пометить эту функцию как мономорфную и оптимизировать ее код. Как вы знаете, только время это происходит, когда функция вызывается с одной и только одной формой.

Давайте проверим, какие формы создаются при использовании каждого из 3 методов копирования:

//testSpread.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objB = { ...objA };
console.log(%HaveSameMap(objA, objB)); // false

//testAssign.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objC = Object.assign({}, objA);
console.log(%HaveSameMap(objA, objC)); // false

//testParse.js
const objA = { name: 'Jack', surname: 'Sparrow' };
const objD = JSON.parse(JSON.stringify(objA));
console.log(%HaveSameMap(objA, objD)); // true

Как видите, JSON.parse(JSON.stringify(objA)) создайте только объект, который имеет ту же форму, что и objA.

Стоимость немономорфных функций

Вот наша функция

function test(obj) {
  let result = '';
  // Any costly operation
  for (let i = 0; i < N; i += 1) {
    result += obj.name + obj.surname;
  }
  return result;
}

Здесь важно то, что функция делает дорогой и блокирует потоки. Этот пример глуп, но представьте, что там происходит какая-то сложная математическая операция. Вот как мы называем нашу функцию двумя разными способами.

const jack = { name: 'Jack', surname: 'Sparrow' };
const frodo = { name: 'Frodo', surname: 'Baggins' };
const charles = { name: 'Charles', surname: 'Xavier' };
const legolas = { name: 'Legolas', surname: 'Thranduilion' };
const indiana = { name: 'Indiana', surname: 'Jones' };

for (let i = 0; i < N; i += 1) {
  test(JSON.parse(JSON.stringify(jack)));
  test(JSON.parse(JSON.stringify(frodo)));
  test(JSON.parse(JSON.stringify(charles)));
  test(JSON.parse(JSON.stringify(legolas)));
  test(JSON.parse(JSON.stringify(indiana)));
}

for (let i = 0; i < N; i += 1) {
  test({ ...jack });
  test({ ...frodo });
  test({ ...charles });
  test({ ...legolas });
  test({ ...indiana });
}

Это довольно распространенный сценарий. Мы не хотим влиять на существующие объекты, поэтому мы решили создать его копию.

Если вы установите N на 10000 и запустить этот цикл, то результат может вас удивить:

test with PARSE: 2522 ms. 
test with spread: 10046 ms.

Какая? spread в 4 раза медленнее, чем JSON.parse? Если это странно, вспомни, что я говорил раньше

V8 собирается пометить эту функцию как мономорфную и оптимизировать ее код

Поскольку тест не простая функция (код простой, но дорогой в выполнении), начальная стоимость вызова JSON.stringify и JSON.parse намного ниже, чем запуск этой функции без оптимизации.

Во втором тестовом прогоне эта функция становится мегаморфной, и V8 прекращает ее оптимизацию. Вы можете проверить этот репозиторий, чтобы попробовать ее на своей машине.

Вывод

Важно понимать, как работает оптимизация JS при разработке сложных методов вычисления в JavaScript. Иногда даже простая вещь может привести к снижению производительности, и вы можете потратить дни, пытаясь выяснить, что происходит.

Я не говорю о замене всех операторов распространения на JSON.parse, это снизит производительность вашего приложения. Я хочу сказать, что иногда снижение производительности одной вещи может значительно улучшить производительность другой.

Случай, который я только что описал, является действительно особенным и может повлиять на вас только тогда, когда функция слишком дорогая, но знание этого может помочь вам по-другому подойти к проблеме.

Источник:

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу