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

    В случае 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%



    График времени на создание одного объекта в зависимости от итерации

    Как видите, во время первого, «холодного» запуска во всех группах создание объекта занимает практически идентичное время. Но уже на второй-третьей итерации скорость выполнения в группах 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%)


    График времени на создание массива объектов в зависимости от его размера

    Как видите, в масштабах всего приложения простая внутренняя оптимизация 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 и оптимизации кода
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      +2

      Чрезвычайно любопытно. В большинстве случаев это конечно станет экономией на спичках, но я определённо учту и в собственном коде и при проведении ревью

        0

        Да, это "экономия на спичках", но в случае JavaScript этих "спичек" очень много :)

          0

          Вот ещё одна такая "спичка": при сдвиге индексации на единицу код может стать в 15 раз быстрее.
          fast by default

        +2

        Так первая рекомендация по написанию оптимального кода: делайте объекты мономорфными, то есть с одинаковым набором свойств в одинаковом порядке.

          +2

          Если реально заморачиваться такой оптимизацией, то без инструмента статического анализа здесь не обойтись. Вручную уследить за порядком инициализации свойств нереально.


          Однако маловероятно, что кто-то откажется от мержа объектов с помощью spread operator из-за нарушения порядка полей.

            +2
            жаль, что сделать замер в реальном приложении, а не синтетическом тесте, слишком сложно. я вот не уверен, что будет заметный прирост. может быть овчинка выделки не стоит.

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

            и не будем забывать, что в будущих версиях V8 эта оптимизация может перестать работать, а «некрасивый» код так и будет мозолить глаза. :)

            и еще, влияет ли эта оптимизация на скорость доступа к свойствам объекта после его создания?
              –1

              Это очень важная и полезная оптимизация на долгоживущих spa приложениях или мобильных версиях (каждый новый объект с новой структурой увеличивает расход памяти на "внутреннее" представление) или серверных приложениях

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

                Это вполне возможно. Самый часто распространенный сценарий: любая функция, принимающая в качестве аргумента объект:


                helper({param1: 1, param2: 2})

                Это ведь тоже объект, который нужно создать перед передачей в функцию. И такие однотипные объекты-аргументы создаются при каждом вызове функции helper.


                влияет ли эта оптимизация на скорость доступа к свойствам объекта после его создания?

                Влияет. См. параграф "Встроенные кэши"

                  0
                  влияет ли эта оптимизация на скорость доступа к свойствам объекта после его создания?

                  Влияет. См. параграф «Встроенные кэши»

                  ага, вот это уже интереснее
                +2

                Судя по тому, что в мире JS никто не обсуждает влияние и механизмы GC, то всем пока не до производительности. Ибо в тех же Java и C# понимание GC одна из важнейших вещей.

                  +3
                  Проблема в том, что реализации GC могут отличаться от браузера к браузеру. А возможности с GC взаимодействовать программно практически нет (только в ноде под флагом --expose_gc). Любой разработчик должен понимать механизмы и влияние GC, это само собой разумеющееся. Но именно обсуждать в разрезе JS фактически нечего.
                    +1
                    Судя по тому, что в мире JS никто не обсуждает влияние и механизмы GC

                    Это не так. Есть много материалов на эту тему. За примером далеко ходить не нужно: управление памятью, четыре вида утечек памяти и борьба с ними. Тут скорее дело в другом. Начиная работать с JS ты можешь не думать или не понимать как работает GC. Но с ростом начнешь этим интересоваться. И хотя прямого управления сборкой мусора у тебя нет, я считаю очень важным понимать как твой код будет на неё влиять.

                    +3
                    Небольшое уточнение:

                    Все это работает и актуально для количества свойств примерно больше 7 (зависит от версии v8). До <= 7 свойства хранятся напрямую в объекте, имеют максимальную скорость к ним доступа и не подвержены снижению производительности из-за порядка

                    v8.dev/blog/fast-properties#the-three-different-kinds-of-named-properties
                      0
                      The number of in-object properties is predetermined by the initial size of the object. If more properties get added than there is space in the object, they are stored in the properties store
                        0
                        все так, именно этот абзац я и имел ввиду
                          0

                          Я так и подумал :) Просто сделал сноску для следующего читателя.

                      –1

                      Как вам такая философия? В условиях экономики например, РФ, спрос падает, кол-во заказов, а соответственно, количество интерактива пользователей снижается, поэтому, оптимизацией заниматься смысла нет. Коду, написанному сейчас, будет предоставлено все больше и больше времени на выполнение в будущем.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое