Насколько важен порядок свойств в объектах JavaScript?

в 8:56, , рубрики: chromium, Google Chrome, javascript, Node, V8, высокая производительность, производительность, тесты производительности

В случае JavaScript-движка V8 — очень даже. В этой статье я привожу результаты своего маленького исследования эффективности одной из внутренних оптимизаций V8.

Описание механизма V8

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

Если очень упростить, то V8 строит объекты используя внутренние скрытые классы. Каждый такой класс соответствует уникальной структуре объекта. Например, если у нас есть такой код:

 // Создаётся базовый скрытый класс, с условным названием C0
const obj = {}

// Создается новый класс с описанием поля "a", условным названием С1 и ссылкой на С0
obj.a = 1

// Создается новый класс с описанием поля "b", условным названием С2 и ссылкой на С1
obj.b = 2

// Создается новый класс с описанием поля "c", условным названием С3 и ссылкой на С2
obj.c = 3

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

// Базовый класс уже существует — C0
const obj2 = {} 

// Класс описывающий поле "a" и ссылающийся на C0 уже существует — С1
obj2.a = 4

// Класс описывающий поле "b" и ссылающийся на C1 уже существует — С2
obj2.b = 5

// Класс описывающий поле "c" и ссылающийся на C2 уже существует — С3
obj2.c = 6

Тем не менее это поведение легко сломать. Достаточно определить такой же объект, но с полями в другом порядке

// Базовый класс уже существует — C0
const obj3 = {} 

// Не существует класса описывающего поле "c" и ссылающегося на C0.
// Будет создан новый — С4
obj3.c = 7

// Не существует класса описывающего поле "a" и ссылающегося на C4.
// Будет создан новый — С5
obj3.a = 8

// Не существует класса описывающего поле "a" и ссылающегося на C5. 
// Будет создан новый — С6
obj3.b = 9

Далее я постарался протестировать и определить разницу в производительности при работе с объектами в которых имена полей совпадают и в которых они отличаются.

Метод тестирования

Я провел три группы тестов:

  1. Static — имена свойств и их порядок не изменялись.
  2. Mixed — изменялась только половина свойств.
  3. Dynamic — все свойства и их порядок были уникальными для каждого объекта.

  • Все тесты запускались на NodeJS версии 13.7.0.
  • Каждый тест запускался в отдельном, новом потоке.
  • Все ключи объектов имеют одинаковую длину, а все свойства одинаковое значение.
  • Для измерения времени выполнения использовался Performance Timing API.
  • В показателях могут быть незначительные колебания. Это вызвано разными фоновыми процессами исполняемыми на машине в тот момент.
  • Код выглядит следующим образом. Ссылку на весь проект дам ниже.
    const keys = getKeys();
    
    performance.mark('start');
    
    const obj = new createObject(keys);
    
    performance.mark('end');
    performance.measure(`${length}`, 'start', 'end');

Результаты

Время на создание одного объекта

Первый, самый главный и простой тест: я взял цикл на 100 итераций. В каждой итерации я создавал новый объект и измерял время на его создание для каждой итерации.

Номер итерации Время выполнения (mks)
Static Mixed Dynamic
1 70,516 74,512 78,131
50 6,247 -91.2% 10,455 -85.9% 41,792 -46.5%
100 5,793 -91.7% 9,845 -86.7% 42,403 -45.7%

Насколько важен порядок свойств в объектах JavaScript? - 1
График времени на создание одного объекта в зависимости от итерации

Как видите, во время первого, «холодного» запуска во всех группах создание объекта занимает практически идентичное время. Но уже на второй-третьей итерации скорость выполнения в группах static и mixed (там, где V8 может применить оптимизацию) по сравнению с группой dynamic значительно вырастает.

И это приводит к тому, что благодаря внутренней оптимизации V8 100-й объект из группы static создаётся в 7 раз быстрее.

Или, можно сказать по другому:

не соблюдение порядка объявления свойств в идентичных объектах может привести к семикратному замедлению вашего кода.

Результат не зависит от того как именно вы создаёте объект. Я испытал несколько разных способов и все они дали практически идентичные значения.

const obj = {}
for (let key of keys) obj[key] = 42

// Тоже самое что и первый вариант, но обёрнуто в функцию-конструктов
const obj = new createObject(keys)

const obj = Object.fromEntries(entries)

const obj = eval('({ ... })')

Суммарное время создания нескольких объектов

В первом тесте мы обнаружили, что объект из группы dynamic может создаваться на 30-40 микросекунд дольше. Но это только один объект. А в реальных приложениях их могут быть сотни или тысячи. Подсчитаем суммарные накладные расходы в разных масштабах. В следующем тесте я буду последовательно повторять первый, но замерять не время на создание одного объекта, а суммарное время на создание массива таких объектов.

Размер массива Время выполнения (ms)
Static Mixed Dynamic
100 0,75 1,29 +0,54 (+72,31%) 4,75 +4,00 (+536,35%)
1000 6,34 12,06 +5,72 (+90,29%) 39,11 +32,78 (+517,35%)
3000 16,41 32,82 +16,42 (+100,07%) 152,48 +136,08 (+829,42%)
10000 38,18 101,84 +63,66 (+166,70%) 428,29 +390,11 (+1021,63%)

Насколько важен порядок свойств в объектах JavaScript? - 2
График времени на создание массива объектов в зависимости от его размера

Как видите, в масштабах всего приложения простая внутренняя оптимизация V8 может дать ускорение в десятки раз.

Где это важно

Повсюду. В JavaScript объекты повсюду. Вот несколько жизненных примеров, где соблюдение порядка свойств даст прирост производительности:

При работе с Fetch

fetch(url1, {headers, method, body})
fetch(url2, {method, headers, body})

При работе с jQuery

$.css({ color, margin })
$.css({ margin, color })

При работе с Vue

Vue.component(name1, {template, data, computed})
Vue.component(name2, {data, computed, template})

При работе с паттерном Composite (компоновщик)

const createAlligator = () => ({...canEat(), ...canPoop()})
const createDog = () => ({...canPoop(), ...canEat()})

И в огромном множестве других мест.

Простое соблюдение порядка свойств в объектах одного типа сделает ваш код более производительным.

Ссылки

Код тестов и подробные результаты
О внутреннем устройстве V8 и оптимизации кода

Автор: Kozack

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js