What is V8?
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. V8 can run standalone, or can be embedded into any C++ application.
Latest posts and feature explainers
- A new way to bring garbage collected programming languages efficiently to WebAssembly 01 November 2023 WebAssembly
- Control-flow Integrity in V8 09 October 2023 security
- Speeding up V8 heap snapshots 27 July 2023 memorytools
- WebAssembly tail calls 06 April 2023 WebAssembly
- Introducing the WebAssembly JavaScript Promise Integration API 19 January 2023 WebAssembly
- Pointer compression in Oilpan 28 November 2022 internalsmemorycppgc
- RegExp v flag with set notation and properties of strings 27 June 2022 ECMAScript
- Discontinuing release blog posts 17 June 2022 release
- Retrofitting temporal memory safety on C++ 14 June 2022 internalsmemorysecurity
- Faster initialization of instances with new class features 20 April 2022 internals
More articles can be found in the blog archive and the features section.
Except as otherwise noted, any code samples from the V8 project are licensed under V8’s BSD-style license. Other content on this page is licensed under the Creative Commons Attribution 3.0 License. For details, see our site policies.
Как работает JavaScript: под капотом движка V8
Сегодня мы заглянем под капот движка JavaScript V8 и выясним, как именно выполняется JavaScript.
Задний план
Веб-стандарты — это набор правил, которые реализует браузер. Они определяют и описывают аспекты Всемирной паутины.
W3C — это международное сообщество, которое разрабатывает открытые стандарты для Интернета. Они следят за тем, чтобы все следовали одним и тем же принципам и не поддерживали десятки совершенно разных сред.
Современный браузер — это довольно сложная программа с кодовой базой, состоящей из десятков миллионов строк кода. Таким образом, он разделен на множество модулей, отвечающих за разную логику.
Двумя наиболее важными частями браузера являются движок JavaScript и движок рендеринга.
Blink — это движок рендеринга, который отвечает за весь конвейер рендеринга, включая деревья DOM, стили, события и интеграцию V8. Он анализирует дерево DOM, определяет стили и определяет визуальную геометрию всех элементов.
Непрерывно отслеживая динамические изменения с помощью кадров анимации, Blink раскрашивает контент на вашем экране. Движок JS — большая часть браузера, но мы еще не вдавались в подробности.
Движок JavaScript 101
Механизм JavaScript выполняет и компилирует JavaScript в собственный машинный код. Каждый крупный браузер разработал свой собственный JS-движок: Google Chrome использует V8, Safari использует JavaScriptCore, а Firefox использует SpiderMonkey.
В частности, мы будем работать с V8, поскольку он используется в Node.js и Electron, но другие движки построены таким же образом.
Каждый шаг будет содержать ссылку на код, отвечающий за него, чтобы вы могли ознакомиться с кодовой базой и продолжить исследование после этой статьи.
Мы будем работать с зеркалом V8 на GitHub, поскольку оно предоставляет удобный и хорошо известный пользовательский интерфейс для навигации по кодовой базе.
Подготовка исходного кода
Первое, что нужно сделать V8, — это загрузить исходный код. Это можно сделать через сеть, кэш или сервис-воркеры.
Как только код получен, нам нужно изменить его так, чтобы компилятор мог его понять. Этот процесс называется парсингом и состоит из двух частей: сканера и самого парсера.
Сканер берет файл JS и преобразует его в список известных токенов. Список всех токенов JS находится в файле keywords.txt.
Анализатор поднимает его вверх и создает абстрактное синтаксическое дерево (AST): древовидное представление исходного кода. Каждый узел дерева обозначает конструкцию, встречающуюся в коде.
Давайте посмотрим на простой пример:
function foo()
Этот код создаст следующую древовидную структуру:
Вы можете выполнить этот код, выполнив обход предварительного заказа (корень, влево, вправо):
- Определите функцию foo .
- Объявите переменную bar .
- Назначьте 1 в bar .
- Верните bar из функции.
Вы также увидите VariableProxy — элемент, который связывает абстрактную переменную с местом в памяти. Процесс разрешения VariableProxy называется анализом объема.
В нашем примере результат процесса VariableProxy будет указывать на одну и ту же переменную bar .
Парадигма Just-in-Time (JIT)
Обычно для выполнения кода язык программирования необходимо преобразовать в машинный код. Есть несколько подходов к тому, как и когда может произойти это преобразование.
Наиболее распространенный способ преобразования кода — выполнение предварительной компиляции. Это работает именно так, как звучит: код преобразуется в машинный код перед выполнением вашей программы на этапе компиляции.
Этот подход используется многими языками программирования, такими как C ++, Java и другими.
С другой стороны таблицы у нас есть интерпретация: каждая строка кода будет выполняться во время выполнения. Этот подход обычно используется в языках с динамической типизацией, таких как JavaScript и Python, поскольку невозможно узнать точный тип до выполнения.
Поскольку предварительная компиляция позволяет оценить весь код вместе, она может обеспечить лучшую оптимизацию и в конечном итоге произвести более производительный код. С другой стороны, интерпретацию проще реализовать, но обычно она медленнее, чем скомпилированный вариант.
Чтобы преобразовать код для динамических языков быстрее и эффективнее, был создан новый подход, названный компиляцией Just-in-Time (JIT). Он сочетает в себе лучшее из интерпретации и компиляции.
Используя интерпретацию как базовый метод, V8 может обнаруживать функции, которые используются чаще, чем другие, и компилировать их, используя информацию о типе из предыдущих выполнений.
Однако есть вероятность, что тип может измениться. Вместо этого нам нужно деоптимизировать скомпилированный код и вернуться к интерпретации (после этого мы можем перекомпилировать функцию после получения обратной связи нового типа).
Давайте рассмотрим каждую часть JIT-компиляции более подробно.
Переводчик
V8 использует интерпретатор под названием Ignition. Первоначально он берет абстрактное синтаксическое дерево и генерирует байтовый код.
Инструкции байтового кода также имеют метаданные, такие как позиции исходной строки для будущей отладки. Как правило, инструкции байтового кода соответствуют абстракциям JS.
Теперь возьмем наш пример и сгенерируем для него байт-код вручную:
LdaSmi #1 // write 1 to accumulator Star r0 // read to r0 (bar) from accumulator Ldar r0 // write from r0 (bar) to accumulator Return // returns accumulator
В Ignition есть так называемый аккумулятор — место, где вы можете хранить / читать значения.
Аккумулятор избавляет от необходимости толкать и выдвигать верхнюю часть стопки. Это также неявный аргумент для многих байт-кодов и обычно содержит результат операции. Return неявно возвращает аккумулятор.
Вы можете проверить весь доступный байтовый код в соответствующем исходном коде. Если вам интересно, как другие концепции JS (например, циклы async / await) представлены в байтовом коде, я считаю полезным прочитать эти ожидания тестирования.
Исполнение
После генерации Ignition интерпретирует инструкции, используя таблицу обработчиков, привязанных к байтовому коду. Для каждого байтового кода Ignition может искать соответствующие функции-обработчики и выполнять их с предоставленными аргументами.
Как мы упоминали ранее, этап выполнения также обеспечивает обратную связь типа о коде. Разберемся, как его собирают и управляют.
Во-первых, мы должны обсудить, как объекты JavaScript могут быть представлены в памяти. При наивном подходе мы можем создать словарь для каждого объекта и связать его с памятью.

Однако обычно у нас много объектов с одинаковой структурой, поэтому было бы неэффективно хранить много дублированных словарей.
Чтобы решить эту проблему, V8 отделяет структуру объекта от самих значений с помощью форм объекта (или внутренних карт) и вектора значений в памяти.
Например, мы создаем литерал объекта:
let c = < x: 3 >let d = < x: 5 >c.y = 4
В первой строке он создаст фигуру Map[c] , имеющую свойство x со смещением 0.
Во второй строке V8 повторно использует ту же форму для новой переменной.
После третьей строки он создаст новую форму Map[c1] для свойства y со смещением 1 и создаст ссылку на предыдущую форму Map[c] .

В приведенном выше примере вы можете видеть, что каждый объект может иметь ссылку на форму объекта, где для каждого имени свойства V8 может найти смещение для значения в памяти.
Формы объектов — это, по сути, связанные списки. Итак, если вы напишете c.x , V8 перейдет к началу списка, найдет там y , перейдет к связанной фигуре и, наконец, получит x и прочитает смещение от нее. Затем он перейдет к вектору памяти и вернет из него первый элемент.
Как вы понимаете, в большом веб-приложении вы увидите огромное количество связанных фигур. В то же время для поиска в связанном списке требуется линейное время, что делает поиск свойств действительно дорогостоящей операцией.
Чтобы решить эту проблему в V8, вы можете использовать встроенный кэш (IC). Он запоминает информацию о том, где найти свойства объектов, чтобы сократить количество поисков.
Вы можете думать об этом как о сайте для прослушивания в вашем коде: он отслеживает все события CALL, STORE и LOAD в функции и записывает все проходящие фигуры.
Структура данных для хранения IC называется вектором обратной связи. Это просто массив для хранения всех микросхем для функции.
function load(a)
Для приведенной выше функции вектор обратной связи будет выглядеть следующим образом:
Это простая функция только с одной ИС, которая имеет тип НАГРУЗКИ и значение UNINIT . Это означает, что он не инициализирован, и мы не знаем, что будет дальше.
Давайте вызовем эту функцию с разными аргументами и посмотрим, как изменится встроенный кэш.
let first = < key: 'first' >// shape A let fast = < key: 'fast' >// the same shape A let slow = < foo: 'slow' >// new shape B load(first) load(fast) load(slow)
После первого вызова функции load наш встроенный кеш получит обновленное значение:
Это значение теперь становится мономорфным, что означает, что этот кеш может разрешиться только для формы A.
После второго вызова V8 проверит значение IC и увидит, что оно мономорфно и имеет ту же форму, что и переменная fast . Таким образом, он быстро вернет смещение и разрешит его.
В третий раз форма отличается от сохраненной. Таким образом, V8 вручную разрешит это и обновит значение до полиморфного состояния с помощью массива из двух возможных форм.
Теперь каждый раз, когда мы вызываем эту функцию, V8 необходимо проверять не только одну форму, но и перебирать несколько вариантов.
Для более быстрого кода вы можете инициализировать объекты одним и тем же типом и не слишком сильно менять их структуру.
Примечание. Вы можете помнить об этом, но не делайте этого, если это приведет к дублированию кода или к менее выразительному коду.
Встроенные кеши также отслеживают, как часто они вызываются, чтобы решить, подходит ли он для оптимизации компилятора — Turbofan.
Компилятор
Зажигание только доходит до нас. Если функция становится достаточно горячей, она будет оптимизирована в компиляторе Turbofan, чтобы сделать ее быстрее.
Турбовентилятор берет байтовый код из Ignition и набирает обратную связь (вектор обратной связи) для функции, применяет набор сокращений на его основе и создает машинный код.
Как мы видели ранее, обратная связь типа не гарантирует, что она не изменится в будущем.
Например, оптимизированный код Turbofan основан на предположении, что некоторое дополнение всегда добавляет целые числа.
Но что было бы, если бы он получил строку? Этот процесс называется деоптимизацией. Мы выбрасываем оптимизированный код, возвращаемся к интерпретируемому коду, возобновляем выполнение и обновляем информацию о типе.
Резюме
В этой статье мы обсудили реализацию JS-движка и точные этапы выполнения JavaScript.
Подводя итог, давайте посмотрим на конвейер компиляции сверху.

Мы рассмотрим это шаг за шагом:
- Все начинается с получения кода JavaScript из сети.
- V8 анализирует исходный код и превращает его в абстрактное синтаксическое дерево (AST).
- На основе этого AST интерпретатор Ignition может начать делать свое дело и создавать байт-код.
- В этот момент движок запускает код и собирает отзывы о типах.
- Чтобы он работал быстрее, байтовый код может быть отправлен оптимизирующему компилятору вместе с данными обратной связи. Оптимизирующий компилятор делает на его основе определенные предположения, а затем создает высокооптимизированный машинный код.
- Если в какой-то момент одно из предположений оказывается неверным, оптимизирующий компилятор деоптимизируется и возвращается к интерпретатору.
Введение
Чтобы разобраться в том, как работает механизм обработки кода (иначе говоря, движок JavaScript), надо понять, что происходит при выполнении кода. Такие знания помогают программистам писать лучший, более быстрый и умный код.
Движки JavaScript — это не что иное, как программы, преобразующие код на JavaScript в код более низкого уровня, который компьютер сможет понять. Эти движки встроены в браузеры и веб-серверы (Node.js), что даёт возможность выполнять код и осуществлять компиляцию во время выполнения.
Разве JavaScript — это не интерпретируемый язык?
Краткий ответ: это зависит от реализации. Обычно JavaScript относят к интерпретируемым языкам, хотя вообще-то он компилируется. Современные компиляторы JavaScript фактически выполняют JIT-компиляцию, т.е. компиляцию «на лету», которая осуществляется во время работы программы.
Движок
Существует множество разных движков. У каждого из них внутри есть что-то вроде конвейера с интерпретатором и конвейера с оптимизатором и компилятором. Интерпретатор генерирует байт-код, а оптимизатор выдаёт оптимизированный машинный код. Далее в статье в качестве примера будет использоваться движок V8.
V8 — это высокопроизводительный движок от Google с открытым исходным кодом JavaScript и WebAssembly, написанный на языке C++. Он используется в Chrome, Node.js и других платформах и реализует ECMAScript и WebAssembly (см. v8.dev).
Что внутри движка
Всякий раз, когда JavaScript-код отправляется в движок V8, он проходит ряд этапов для отображения console.log:
Парсер
Движок выполняет то, что мы называем лексическим анализом. Это первое, что происходит с файлом JavaScript при попадании в движок. Код разбивается на части, называемые токенами, для выявления их назначения, после чего мы узнаём, что код пытается сделать.
Абстрактное синтаксическое дерево (AST)
На основе этих токенов создаётся то, что мы называем AST. Синтаксическое дерево — это древовидное представление синтаксической структуры кода JavaScript, и мы можем использовать этот инструмент для анализа преобразования кода AST.
Интерпретатор
Интерпретатор читает файлы JavaScript построчно и преобразовывает их на ходу (во время работы программы, не прерывая её выполнение). На основе сгенерированного кода AST интерпретатор начинает быстро создавать байт-код. Никаких оптимизаций здесь не выполняется, так что байт-код этот неоптимизированный.
Байт-код не является таким низкоуровневым, как машинный код, но всё же может быть интерпретирован движком JavaScript для выполнения кода.
Профайлер
Профайлер отвечает за проверку кода. Он вызывает специальное средство контроля, которое отслеживает код и наблюдает за ходом его выполнения, обращая наше внимание на то, как можно оптимизировать код. Выдаёт, например, информацию о том, сколько раз код запускался, какие типы используются и как мы можем его оптимизировать?
Так что, если профайлер находит часть кода, которую можно оптимизировать, он передаёт этот код JIT-компилятору, чтобы он мог быть скомпилирован и выполнялся быстрее. Рассмотрим эти конвейеры с интерпретатором и компилятором более подробно.
Оптимизирующий компилятор
Задача оптимизирующего компилятора — определить, что делает программа, подлежащая оптимизации, и создать из неё оптимизированную программу, выполняющую всё то же самое, только быстрее.
Он не преобразует файлы на лету. Он делает свою работу заранее, создавая преобразование только что написанного кода и компилируя его в язык, понятный для компьютера.
Деоптимизация
Оптимизирующий компилятор на основе имеющихся у него данных профилирования делает определённые предположения, а затем выдаёт высокооптимизированный машинный код. Но вполне возможно, что в какой-то момент одно из предположений окажется неверным. И тогда оптимизирующий компилятор может деоптимизировать код.
Объектная модель JavaScript
JavaScript — это динамический язык программирования, а это подразумевает, что свойства могут легко добавляться или удаляться из объекта после его создания. Для написания более лучшего кода надо понимать, как JavaScript определяет объекты и как движок работает с объектами и свойствами.
- Enumerable → определяет, перечисляется ли свойство в циклах for — in .
- Value → само значение.
- Writable → определяет, можно ли свойство изменить.
- Configurable → определяет, можно ли удалить свойство.
Оптимизация доступа к свойству
В динамических языках, таких как JavaScript, для доступа к свойствам требуется ряд инструкций. Поэтому почти в каждом движке имеется оптимизация для ускорения этого доступа. Такая оптимизация в V8 реализуется скрытыми классами: V8 присоединяет скрытый класс к каждому отдельному объекту. Целью скрытых классов является оптимизация времени доступа к свойствам.
Скрытые классы работают аналогично фиксированным макетам (классам) объектов, используемым в таких языках, как Java (за исключением того, что они создаются во время выполнения). Магия здесь в том, что несколько объектов могут иметь один и тот же скрытый класс, поэтому необходимы только минимальные ресурсы, а код становится быстрее:
Выгода становится очевидной, когда объектов много. Ведь пока у них один и тот же скрытый класс, информацию приходится сохранять только один раз независимо от того, сколько всего имеется объектов!
Тема оптимизации очень обширна и может стать предметом обсуждения отдельной статьи.
Встроенные кэши
Цель встроенного кэширования в том, чтобы ускорить привязку метода времени выполнения. Происходит это за счёт запоминания результатов поиска предыдущего метода непосредственно в месте вызова. Встроенное кэширование особенно полезно для динамически типизированных языков, где большинство, если не все привязки методов происходят во время выполнения и где виртуальные таблицы методов часто не могут быть использованы.
Основная причина существования скрытых классов — концепция встроенных кэшей. Движки JavaScript используют встроенные кэши для запоминания информации о том, где найти свойства в объектах. Поэтому нет необходимости повторять дорогостоящий поиск свойств при каждом доступе к ним. Зачем каждый раз искать свойства, когда со встроенными кэшами это значительно быстрее?
Выводы
- инициализировать объекты лучше всегда одним и тем же способом, чтобы скрытые классы у них не были разными;
- с атрибутами свойств элементов массива надо быть осторожным, чтобы они могли аккуратно сохраняться, а работа с ними была эффективной.
- Как работает новый await верхнего уровня в JavaScript
- Чистый код JavaScript — Вертикальное форматирование
- Что значит this в JavaSсript?
Как работает JS: о внутреннем устройстве V8 и оптимизации кода
Перед вами — второй материал из серии, посвящённой особенностям работы JavaScript на примере движка V8. В первом шла речь о механизмах времени выполнения V8 и о стеке вызовов. Сегодня мы углубимся в особенности V8, благодаря которым исходный код на JS превращается в исполняемую программу, и поделимся советами по оптимизации кода.
О JS-движках
JavaScript-движок — это программа, или, другими словами, интерпретатор, выполняющий код, написанный на JavaScript. Движок может быть реализован с использованием различных подходов: в виде обычного интерпретатора, в виде динамического компилятора (или JIT-компилятора), который, перед выполнением программы, преобразует исходный код на JS в байт-код некоего формата.
Вот список популярных реализаций JavaScript-движков.
- V8 — движок с открытым исходным кодом, написан на C++, его разработкой занимается Google.
- Rhino — этот движок с открытым кодом поддерживает Mozilla Foundation, он полностью написан на Java.
- SpiderMonkey — это самый первый из появившихся JS-движков, который в прошлом применялся в браузере Netscape Navigator, а сегодня — в Firefox.
- JavaScriptCore — ещё один движок с открытым кодом, известный как Nitro и разрабатываемый Apple для браузера Safari.
- KJS — JS-движок KDE, который разработал Гарри Портен для браузера Konqueror, входящего в проект KDE.
- Chakra (JScript9) — движок для Internet Explorer.
- Chakra (JavaScript) — движок для Microsoft Edge.
- Nashorn — движок с открытым кодом, являющийся частью OpenJDK, которым занимается Oracle.
- JerryScript — легковесный движок для интернета вещей.
Почему был создан движок V8?

Движок с открытым кодом V8 был создан компанией Google, он написан на C++. Движок используется в браузере Google Chrome. Кроме того, что отличает V8 от других движков, он применяется в популярной серверной среде Node.js.
Логотип V8
При проектировании V8 разработчики задались целью улучшить производительность JavaScript в браузерах. Для того, чтобы добиться высокой скорости выполнения программ, V8 транслирует JS-код в более эффективный машинный код, не используя интерпретатор. Движок компилирует JavaScript-код в машинные инструкции в ходе исполнения программы, реализуя механизм динамической компиляции, как и многие современные JavaScript-движки, например, SpiderMonkey и Rhino (Mozilla). Основное различие заключается в том, что V8 не использует при исполнении JS-программ байт-код или любой промежуточный код.
О двух компиляторах, которые использовались в V8
Внутреннее устройство V8 изменилось с выходом версии 5.9, которая появилась совсем недавно. До этого же он использовал два компилятора:
- full-codegen — простой и очень быстрый компилятор, который выдаёт сравнительно медленный машинный код.
- Crankshaft — более сложный оптимизирующий JIT-компилятор, который генерирует хорошо оптимизированный код.
- Главный поток, который занимается тем, что от него можно ожидать: читает исходный JS-код, компилирует его и выполняет.
- Поток компиляции, который занимается оптимизацией кода в то время, когда выполняется главный поток.
- Поток профилировщика, который сообщает системе о том, в каких методах программа тратит больше всего времени, как результат, Crankshaft может эти методы оптимизировать.
- Несколько потоков, которые поддерживают механизм сборки мусора.
После того, как код какое-то время поработает, поток профилировщика соберёт достаточно данных для того, чтобы система могла понять, какие методы нужно оптимизировать.
Далее, в другом потоке, начинается оптимизация с помощью Crankshaft. Он преобразует абстрактное синтаксическое дерево JavaScript в высокоуровневое представление, использующее модель единственного статического присваивания (static single-assignment, SSA). Это представление называется Hydrogen. Затем Crankshaft пытается оптимизировать граф потока управления Hydrogen. Большинство оптимизаций выполняется на этом уровне.
Встраивание кода
Первая оптимизация программы заключается в заблаговременном встраивании в места вызовов как можно большего объёма кода. Встраивание кода — это процесс замены команды вызова функции (строки, где вызывается функция) на её тело. Этот простой шаг позволяет сделать следующие оптимизации более результативными.

Вызов функции заменяется на её тело
Скрытые классы
JavaScript — это язык, основанный на прототипах: здесь нет классов. Объекты здесь создаются с использованием процесса клонирования. Кроме того, JS — это динамический язык программирования, это значит, что, после создания экземпляра объекта, к нему можно добавлять новые свойства и удалять из него существующие.
Большинство JS-интерпретаторов используют структуры, напоминающие словари (основанные на использовании хэш-функций), для хранения сведений о месте расположения значений свойств объектов в памяти. Использование подобных структур делает извлечение значений свойств в JavaScript более сложной задачей, чем в нединамических языках, таких, как Java и C#. В Java, например, все свойства объекта определяются не изменяющейся после компиляции программы схемой объекта, их нельзя динамически добавлять или удалять (надо отметить, что в C# есть динамический тип, но тут мы можем не обращать на это внимание). Как результат, значения свойств (или указатели на эти свойства) могут быть сохранены, с фиксированным смещением, в виде непрерывного буфера в памяти. Шаг смещения можно легко определить, основываясь на типе свойства, в то время как в JavaScript это невозможно, так как тип свойства может меняться в процессе выполнения программы.
Так как использование словарей для выяснения адресов свойств объекта в памяти очень неэффективно, V8 использует вместо этого другой метод: скрытые классы. Скрытые классы похожи на обычные классы в типичном объектно-ориентированном языке программирования, вроде Java, за исключением того, что создаются они во время выполнения программы. Посмотрим, как всё это работает, на следующем примере:
function Point(x, y) < this.x = x; this.y = y; >var p1 = new Point(1, 2);

Когда происходит вызов new Point(1, 2) , V8 создаёт скрытый класс C0 .
Первый скрытый класс С0
Пока, ещё до выполнения конструктора, у объекта Point нет свойств, поэтому класс C0 пуст.
Как только будет выполнена первая команда в функции Point , V8 создаст второй скрытый класс, C1 , который основан на C0 . C1 описывает место в памяти (относительно указателя объекта), где можно найти свойство x . В данном случае свойство x хранится по смещению 0, что означает, что если рассматривать объект Point в памяти как непрерывный буфер, первое смещение соответствует свойству x . Кроме того, V8 добавит в класс C0 сведения о переходе к классу C1 , где указывается, что если к объекту Point будет добавлено свойство x , скрытый класс нужно изменить с C0 на C1 . Скрытый класс для объекта Point , как показано на рисунке ниже, теперь стал классом С1 .

Каждый раз, когда к объекту добавляется новое свойство, в старый скрытый класс добавляются сведения о переходе к новому скрытому классу. Переходы между скрытыми классами важны, так как они позволяют объектам, которые создаются одинаково, иметь одни и те же скрытые классы. Если два объекта имеют общий скрытый класс и к ним добавляется одно и то же свойство, переходы обеспечат то, что оба объекта получат одинаковый новый скрытый класс и весь оптимизированный код, который идёт вместе с ним.
Этот процесс повторяется при выполнении команды this.y = y (опять же, делается это внутри функции Point , после вышеописанной команды по добавлению свойства x ).
Тут создаётся новый скрытый класс, C2 , а в класс C1 добавляются сведения о переходе, где указывается, что если к объекту Point добавляется свойство y (при этом речь идёт об объекте, который уже содержит свойство x ), тогда скрытый класс объекта должен измениться на C2 .

Переход к использованию класса C2 после добавления к объекту свойства y
Переходы между скрытыми классами зависят от порядка, в котором к объекту добавляются свойства. Взгляните на этот пример кода:
function Point(x, y) < this.x = x; this.y = y; >var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 4); p2.b = 7; p2.a = 8;
В подобной ситуации можно предположить, что у объектов p1 и p2 будет один и тот же скрытый класс и одно и то же дерево переходов скрытых классов. Однако, на самом деле это не так. В объект p1 первым добавляется свойство a , а затем — свойство b . В объект p2 сначала добавляют свойство b , а затем — a . В результате объекты p1 и p2 будут иметь различные скрытые классы — результат различных путей переходов между скрытыми классами. В подобных случаях гораздо лучше инициализировать динамические свойства в одном и том же порядке для того, чтобы скрытые классы могли быть использованы повторно.
Встроенные кэши
V8 использует и другую технику для оптимизации выполнения динамически типизированных языков, называемую встроенным кэшем вызовов. Встроенное кэширование основано на наблюдении, которое заключается в том, что повторяющиеся обращения к одному и тому же методу имеют тенденцию происходить с использованием объектов одного типа. Более подробно об этом можно почитать здесь. Если у вас нет времени слишком в это углубляться, читая вышеупомянутый материал, здесь мы изложим концепцию встроенного кэширования буквально в двух словах.
Итак, как же всё это работает? V8 поддерживает кэш типов объектов, которые мы передали в качестве параметра недавно вызванным методам, и использует эту информацию для того, чтобы сделать предположение о типах объектов, которые будут переданы как параметры в будущем. Если V8 смог сделать правильное предположение о типе объекта, который будет передан методу, он может пропустить процесс выяснения того, как получать доступ к свойствам объекта, а, вместо этого, использовать сохранённую информацию из предыдущих обращений к скрытому классу объекта.
Как связаны концепции скрытых классов и встроенных кэшей вызовов? Когда метод вызывается для некоего объекта, движок V8 должен обратиться к скрытому классу этого объекта для того, чтобы определить смещение для доступа к конкретному свойству. После двух успешных обращений одного и того же метода к одному и тому же скрытому классу, V8 опускает операцию обращения к скрытому классу и просто добавляет сведения о смещении свойства к самому указателю объекта. Выполняя вызовов этого метода в будущем, V8 предполагает , что скрытый класс не менялся и идёт прямо по адресу памяти для конкретного свойства, используя смещение, сохранённое после предыдущих обращений к скрытому классу. Это очень сильно увеличивает скорость выполнения кода.
Встроенное кэширование вызовов, кроме того, является причиной того, почему так важно, чтобы объекты одного и того же типа использовали общие скрытые классы. Если вы создаёте два объекта одинакового типа, но с разными скрытыми классами (как сделано в примере выше), V8 не сможет использовать встроенное кэширование, так как, даже хотя объекты имеют один и тот же тип, в соответствующих им скрытых классах назначено разное смещение их свойствам.

Перед нами объекты одного типа, но их свойства a и b были созданы в разном порядке и имеют разное смещение
Компиляция в машинный код
Как только граф Hydrogen оптимизирован, Crankshaft переводит его в низкоуровневое представление, которое называется Lithium. Большинство реализаций Lithium зависимо от архитектуры системы. На этом уровне, например, происходит выделение регистров.
В итоге Lithium-представление компилируется в машинный код. Затем происходит то, что называется замещением в стеке (on-stack replacement, OSR). Перед компиляцией и оптимизацией методов, в которых программа тратит много времени, нужно будет поработать с их неоптимизированными вариантами. Затем, не прерывая работу, V8 трансформирует контекст (стек, регистры) таким образом, чтобы можно было переключиться на оптимизированную версию кода. Это очень сложная задача, учитывая то, что помимо других оптимизаций, V8 изначально выполняет встраивание кода. V8 — не единственный движок, способный это сделать.
Как быть, если оптимизация оказалась неудачной? От этого есть защита — так называемая деоптимизация. Она направлена на обратную трансформацию, возвращающую систему к использованию неоптимизированного кода в том случае, если предположения, сделанные движком и положенные в основу оптимизации, больше не соответствуют действительности.
Сборка мусора
Для сборки мусора V8 использует традиционный генеалогический подход «пометь и выброси» (mark-and-sweep) для маркировки и очистки предыдущих поколений кода. Фаза маркировки предполагает остановку выполнения JavaScript. Для того, чтобы контролировать нагрузку на систему, создаваемую сборщиком мусора и сделать выполнение кода более стабильным, V8 использует инкрементный алгоритм маркирования: вместо того, чтобы обходить всю кучу, он пытается пометить всё, что сможет, обходя лишь часть кучи. Затем нормальное выполнение кода возобновляется. Следующий проход сборщика мусора по куче начинается там, где закончился предыдущий. Это позволяет добиться очень коротких пауз в ходе обычного выполнения кода. Как уже было сказано, фазой очистки памяти занимаются отдельные потоки.
Ignition и TurboFan
С выходом в этом году V8 версии 5.9. был представлен и новый конвейер выполнения кода. Этот конвейер позволяет достичь ещё большего улучшения производительности и значительной экономии памяти, причём, не в тестах, а в реальных JavaScript-приложениях.
Новая система построена на базе интерпретатора Ingnition и новейшего оптимизирующего компилятора TurboFan. Подробности об этих новых механизмах V8 можно почитать в этом материале.
С выходом V8 5.9 full-codegen и Crankshaft (технологии, которые использовались в V8 с 2010-го года) больше применяться не будут. Команда V8 развивает новые средства, стараясь не отстать от новых возможностей JavaScript и внедрить оптимизации, необходимые для поддержки этих возможностей. Переход на новые технологии и отказ от поддержки старых механизмов означает развитие V8 в сторону более простой и хорошо управляемой архитектуры.

Улучшения в тестах производительности для браузерного и серверного вариантов использования JS
Эти улучшения — лишь начало. Новый конвейер выполнения кода на основе Ignition и TurboFan открывает путь к дальнейшим оптимизациям, которые улучшат производительность JavaScript и сделают V8 экономичнее.
Мы рассмотрели некоторые особенности V8, а теперь приведём несколько советов по оптимизации кода. На самом деле, кстати, всё это вполне можно вывести из того, о чём мы говорили выше.
Подходы к оптимизации JavaScript-кода для V8
- Порядок свойств объектов. Всегда инициализируйте свойства объектов в одном и том же порядке. Нужно это для того, чтобы одинаковые объекты использовали одни и те же скрытые классы, и, как следствие, оптимизированный код.
- Динамические свойства. Добавление свойств к объектам после создания экземпляра объекта приведёт к изменению скрытого класса и к замедлению методов, которые были оптимизированы для скрытого класса, используемого объектами ранее. Вместо добавления свойств динамически, назначайте их в конструкторе объекта.
- Методы. Код, который несколько раз вызывает один и тот же метод, будет выполняться быстрее, чем код, который вызывает несколько разных методов по одному разу (из-за встроенных кэшей).
- Массивы. Избегайте разреженных массивов, ключи которых не являются последовательными числами. Разреженный массив, то есть массив, некоторые из элементов которого отсутствуют, будет обрабатываться системой как хэш-таблица. Для доступа к элементам такого массива требуется больше вычислительных ресурсов. Кроме того, постарайтесь избежать заблаговременного выделения памяти под большие массивы. Лучше, если их размер будет увеличиваться по мере надобности. И, наконец, не удаляйте элементы в массивах. Из-за этого они превращаются в разреженные массивы.
- Числа. V8 представляет числа и указатели на объекты, используя 32 бита. Он задействует один бит для того, чтобы определить, является ли некое 32-битное значение указателем на объект (флаг — 1), или целым числом (флаг — 0), которое называется маленьким целым числом (SMall Integer, SMI) из-за того, что его длина составляет 31 бит. Если для хранения числового значения требуется более 31 бита, V8 упакует число, превратив его в число двойной точности и создаст новый объект для того, чтобы поместить в него это число. Постарайтесь использовать 31-битные числа со знаком везде, где это возможно, для того, чтобы избежать ресурсоёмких операций упаковки чисел в JS-объекты.
Итоги
Мы, в SessionStack, стараемся следовать вышеизложенным принципам при написании JS-кода. Надеемся, немного разобравшись в том, как работают внутренние механизмы V8, и учтя то, что мы рассказали выше, вы сможете улучшить качество и производительность ваших программ.
Уважаемые читатели! Какими советами по оптимизации JS-кода можете поделиться вы?
- Блог компании RUVDS.com
- Веб-разработка
- JavaScript
