Что такое v8 в javascript
Перейти к содержимому

Что такое v8 в javascript

  • автор:

Как работает JavaScript: внутри движка V8 + 5 советов по написанию оптимизированного кода

Несколько недель назад мы начали серию, цель которой — более глубокое изучение JavaScript и того, как он на самом деле работает: мы подумали, что, зная строительные блоки JavaScript и то, как они взаимодействуют друг с другом, вы сможете писать более качественный код и приложения.

Первый пост серии сфокусирован на обзоре движка, времени выполнения и стека вызовов. Этот второй пост будет посвящен внутренним частям движка Google V8 JavaScript. Мы также дадим несколько быстрых советов о том, как писать более качественный код JavaScript — лучшие практики, которым руководствуется наша команда разработчиков SessionStack при создании продукта. (Также рекомендую руководство по написанию качественного кода от Airbnb. Прим. переводчика).

Обзор

Движок JavaScript — это программа или интерпретатор, который выполняет JavaScript код. Движок JavaScript может быть реализован в виде стандартного интерпретатора или JIT (Just-In-Time) компилятора, который компилирует JavaScript в байт-код.

Ниже представлен список популярных проектов, которые реализуют движок JavaScript:

· V8 — открытый исходный код, разработан компанией Google, написан на C++

· Rhino — управляемый Mozilla Foundation, открытый исходный код, разработан преимущественно на Java

· SpiderMonkey — первый движок JavaScript, который был ранее разработан Netscape Navigator, а сейчас над ним работает Firefox

· JavaScriptCore — открытый исходный код, продавался под именем Nitro и разрабатывался компанией Apple для Safari

· KJS — движок первоначально разработанный Harri Porten для KDE проекта — браузера Konqueror

· Chakra (JScript9) — в браузере Internet Explorer

· Chakra (JavaScript) — в браузере Microsoft Edge

· Nashorn — открытый исходный код как часть OpenJDK, написанный на Oracle Java Languages и Tool Group

· JerryScript — легковесный движок для Интернета вещей.

Зачем был создан движок V8?

Механизм V8, созданный Google, имеет открытый исходный код и написан на C ++. Этот движок используется внутри Google Chrome. В отличие от остальных двигателей, V8 также используется для популярной среды выполнения Node.js.

V8 впервые был разработан, с целью повышения производительности исполнения JavaScript в веб-браузерах. Чтобы добиться скорости, V8 переводит код JavaScript в более эффективный машинный код, вместо использования интерпретатора. Он компилирует код JavaScript в машинный код во время исполнения путем реализации JIT (Just-In-Time) компилятора, как это делают многие современные механизмы JavaScript, такие как SpiderMonkey или Rhino (Mozilla). Основное отличие состоит в том, что V8 не производит байт-код или какой-либо промежуточный код.

В V8 было два компилятора

До выхода версии 5.9 V8 в движке использовались два компилятора:

· full-codegen — простой и очень быстрый компилятор, который генерирует простой и относительно медленный машинный код.
· Crankshaft — более сложный (Just-In-Time) оптимизирующий компилятор, который генерирует высоко-оптимизированный код.

Движок V8 также использует несколько потоков внутри:

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

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

Когда ваш код выполняется в течение некоторого времени, поток Profiler собрал достаточно данных, чтобы указать, какой метод следует оптимизировать.

Далее, оптимизация Crankshaft начинается в другом потоке. Он переводит дерево абстрактного синтаксиса JavaScript в высоко-уровневое статическое представление с единым назначением (SSA) под названием Hydrogen и пытается оптимизировать этот граф Hydrogen. Большая часть оптимизации осуществляется на этом этапе.

Встраивание

Первая оптимизация включает в себя как можно больше кода изначально. Встраивание — это процесс замены call site (строки кода, где вызывается функция) телом вызываемой функции. Этот простой шаг позволяет после оптимизации быть более значимым.

Скрытый класс

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

Большинство интерпретаторов JavaScript используют словарные структуры (на основе хэш-функции) для хранения расположения значений свойств объекта в памяти. Эта структура делает получение значения в 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”.

Для Point еще не определены свойства, поэтому “C0” — пустой.

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

Этот процесс повторяется, когда выполняется оператор “this.y = y” (опять же, внутри функции Point, после оператора “this.x = x”).

Создается новый скрытый класс с именем “C2”, к “C1” добавляется переход класса, в котором говорится, что если свойство “y” добавлено к объекту Point (который уже содержит свойство “x”), то скрытый класс должен измениться на “C2”, а скрытый класс указанного объекта обновляется до “C2”.

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

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 не сможет использовать встроенное кэширование, потому что даже если два объекта одного типа, их соответствующие скрытые классы назначить различные смещения для их свойств.

Компиляция в машинный код

Как только граф Hydrogen оптимизирован, Crankshaft понижает его до представления более низкого уровня, называемого Lithium. Большая часть реализации Lithium зависит от архитектуры. Распределение регистров происходит на этом уровне.

В итоге, Lithium компилируется в машинный код. Затем происходит нечто другое, называемое OSR: замена в стеке. Прежде чем мы начали компилировать и оптимизировать явно длительный метод, мы, скорее всего, его запускали. V8 не собирается забывать, что он просто медленно выполняется, чтобы начать снова с оптимизированной версией. Вместо этого он преобразует весь имеющийся у нас контекст (стек, регистры), чтобы мы могли переключиться на оптимизированную версию в середине выполнения. Это очень сложная задача, учитывая, что среди других оптимизаций, V8 изначально встроил код. V8 — не единственный движок, способный сделать это.

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

Сборщик мусора

Для сбора мусора в V8 используется традиционный подход «маркировка и подметание» для очистки старого поколения. Фаза «маркировки» должна остановить выполнение JavaScript. Чтобы контролировать затраты на сборку мусора и сделать выполнение более стабильным, V8 использует добавочную маркировку: вместо обхода всей кучи, пытаясь пометить каждый возможный объект, он проходит только часть кучи, а затем возобновляет нормальное выполнение. Следующая остановка сборщика мусора будет продолжаться с того места, где остановился предыдущий проход по куче. Это позволяет делать очень короткие паузы во время обычного исполнения. Как упоминалось ранее, фаза «подметания» обрабатывается отдельными потоками.

Ignition и TurboFan

С выпуском V8 5.9 ранее в 2017 году был представлен новый pipeline выполнения. Этот новый pipeline обеспечивает еще большее повышение производительности и значительную экономию памяти в реальных приложениях JavaScript.

Новый pipeline выполнения построен на базе Ignition, интерпретатора V8, и TurboFan, новейшего оптимизирующего компилятора V8.

Вы можете проверить сообщение в блоге от команды V8 о теме здесь.

С момента выхода версии 5.9 V8 full-codegen и Crankshaft (технологии, которые обслуживают V8 с 2010 года) больше не используются V8 для выполнения JavaScript, так как команда V8 изо всех сил пытается идти в ногу с новыми функциями языка JavaScript и оптимизации, необходимые для этих функций.

Новый конвейер выполнения построен на основе Ignition, интерпретатора V8, и TurboFan, новейшего оптимизирующего компилятора V8.

Вы можете проверить сообщение в блоге от команды V8 о теме здесь.
С момента выхода версии 5.9 V8 full-codegen и Crankshaft (технологии, которые обслуживают V8 с 2010 года) больше не используются V8 для выполнения JavaScript, так как команда V8 изо всех сил пытается идти в ногу с новыми функциями языка JavaScript и оптимизации, необходимые для этих функций.

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

Эти улучшения — только начало. Новый Ignition и TurboFan pipeline открывает путь для дальнейшей оптимизации, которая повысит производительность JavaScript и сократит присутствие V8 в Chrome и Node.js в ближайшие годы.

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

Как писать оптимизированный JavaScript

  1. Порядок свойств объекта: всегда создавайте экземпляры свойств вашего объекта в одном и том же порядке, чтобы скрытые классы, и впоследствии оптимизированный код, могли совместно использоваться.
  2. Динамические свойства: добавление свойств к объекту, после создания экземпляра, вызовет изменение скрытого класса и замедлит все методы, которые были оптимизированы для предыдущего скрытого класса. Вместо этого назначьте все свойства объекта в его конструкторе.
  3. Методы: код, который выполняет один и тот же метод несколько раз, будет выполняться быстрее, чем код, который выполняет много разных методов только один раз (из-за встроенного кэширования).
  4. Массивы: избегайте разреженных массивов, где ключи не являются инкрементными числами. Разреженные массивы, в которых нет каждого элемента, являются хэш-таблицей. Элементы в таких массивах более дороги для доступа. Также старайтесь избегать предварительного выделения больших массивов. Лучше расти, как вы идете. Наконец, не удаляйте элементы в массивах. Это делает ключи разреженными.
  5. Помеченные значения: V8 представляет объекты и числа с 32 битами. Он использует бит, чтобы узнать, является ли он объектом (flag = 1), или целым числом (flag = 0), называемым SMI (SMall Integer) из-за его 31 бита. Затем, если числовое значение больше 31 бита, V8 укажет число, превратив его в двойное и создав новый объект для помещения числа внутрь. Старайтесь по возможности использовать 31-разрядные числа со знаком, чтобы избежать дорогостоящей операции упаковки в объект JS.

Мы в SessionStack стараемся следовать этим рекомендациям при написании высоко оптимизированного кода JavaScript. Причина в том, что, как только вы интегрируете SessionStack в ваше производственное веб-приложение, оно начинает записывать все: все изменения DOM, взаимодействия с пользователем, исключения JavaScript, трассировки стека, неудачные сетевые запросы и сообщения отладки.
С помощью SessionStack вы можете воспроизводить проблемы в своих веб-приложениях в виде видео и видеть все, что случилось с вашим пользователем. И все это должно происходить без ущерба для производительности вашего веб-приложения.
Существует бесплатный план, который позволяет начать бесплатно.

Источники

  • https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P—dtDvwXXEeD0/pub
  • https://github.com/thlorenz/v8-perf
  • http://code.google.com/p/v8/wiki/UsingGit
  • http://mrale.ph/v8/resources.html
  • https://www.youtube.com/watch?v=UJPdhx5zTaw
  • https://www.youtube.com/watch?v=hWhMKalEicY

Как работает 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

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

Итоги

Мы, в SessionStack, стараемся следовать вышеизложенным принципам при написании JS-кода. Надеемся, немного разобравшись в том, как работают внутренние механизмы V8, и учтя то, что мы рассказали выше, вы сможете улучшить качество и производительность ваших программ.

Уважаемые читатели! Какими советами по оптимизации JS-кода можете поделиться вы?

  • Блог компании RUVDS.com
  • Веб-разработка
  • JavaScript

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

  1. Control-flow Integrity in V8 09 October 2023 security
  2. Speeding up V8 heap snapshots 27 July 2023 memorytools
  3. WebAssembly tail calls 06 April 2023 WebAssembly
  4. Introducing the WebAssembly JavaScript Promise Integration API 19 January 2023 WebAssembly
  5. Pointer compression in Oilpan 28 November 2022 internalsmemorycppgc
  6. RegExp v flag with set notation and properties of strings 27 June 2022 ECMAScript
  7. Discontinuing release blog posts 17 June 2022 release
  8. Retrofitting temporal memory safety on C++ 14 June 2022 internalsmemorysecurity
  9. Faster initialization of instances with new class features 20 April 2022 internals
  10. V8 release v9.9 31 January 2022 release

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: часть первая

Внутреннее устройство JavaScript и движка V8: что нужно знать, чтобы писать быстрый и правильный код.

23 октября 2017 13 минут 32244

Автор статьи
Андрей Никифоров

Автор статьи
Андрей Никифоров
https://gbcdn.mrgcdn.ru/uploads/post/1261/og_cover_image/f9c84e5bafe3d77a721d45b18463dbb4

JavaScript — самый популярный язык программирования в репозиториях Гитхаба. Любой фронтенд-разработчик имеет с ним дело, а Node.js активно используется в бэкенд-разработке. Но понимаете ли вы, как на самом деле устроен JavaScript?

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

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

Как работает JavaScript: движок, рантайм, стек вызовов

По мере роста популярности JavaScript команды разработчиков используют его на многих уровнях своих приложений: на веб-страницах, в бэкенде, гибридных приложениях, на мобильных устройствах. Как можно увидеть на Github Stats, JavaScript сейчас на первом месте рейтинга по количеству активных репозиториев и общему количеству пушей, и не сильно отстает в других категориях.

Прим. переводчика: вот актуальная статистика по языкам: madnight.github.io/githut

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

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

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

Если вы относительно недавно знакомы с JavaScript, этот пост поможет вам понять, почему JavaScript такой «странный» относительно других языков. А если вы уже опытный JavaScript–разработчик, я надеюсь, что статья покажет что-то новое о том, как работает язык, с которым вы работаете каждый день.

Движок JavaScript

Популярный пример движка JavaScript — это V8 от Google. Он используется, к примеру, внутри Chrome и Node.js. Вот сильно упрощенная модель того, как он выглядит изнутри:

Движок состоит из двух основных частей:

  • Куча — место, где происходит выделение памяти
  • Стек вызовов — место, где выполняется код, организованный в кадры вызовов

Среда выполнения

В браузере есть куча API, которыми пользуется почти каждый разработчик, например setTimeout. Эти API, однако, не предоставляются движком. Так откуда они берутся? Оказывается, что реальность несколько сложнее.

Итак, у нас есть движок, но кроме этого еще куча всего. У нас есть эти штуки, которые мы называем Web API, предоставляемые браузером, всякие DOM, AJAX, setTimeout и еще много всего. И еще у нас есть петля событий и очередь обратных вызовов.

Стек вызовов

JavaScript — однопоточный язык, и это значит — один стек вызовов. Иными словами, одна операция в один момент времени.

Стек вызовов — структура данных, которая попросту хранит информацию о том, какой участок кода выполняется сейчас. Если мы входим в функцию, мы помещаем ее в стек. Когда мы возвращаемся из функции, мы удаляем ее из стека. Это все, что делает стек.

Давайте разберемся на примере. Посмотрите на код:

function multiply(x, y) < return x * y; > function printSquare(x) < var s = multiply(x, x); console.log(s); > printSquare(5);

В начале выполнения стек пустой. После этого идут такие шаги:

Каждый элемент в стеке называется кадром вызова. Стектрейс, который выводится при выбрасывании исключения — по сути состояние стека вызовов на момент исключения. Посмотрите код:

function foo() < throw new Error('SessionStack will help you resolve crashes :)'); > function bar() < foo(); >function start() < bar(); >start();

В Chrome вы увидите такую картинку:

«Раздувание стека» — это то, что случается, когда стек увеличивается до его максимума. И это может произойти очень просто, особенно при использовании рекурсии без должного тестирования. Вот пример:

function foo() < foo(); >foo();

Когда движок исполняет этот код, он сперва вызывает функцию «foo». Эта функция рекурсивна, и постоянно вызывает саму себя без условия остановки. Так что на каждом шаге одна и та же функция добавляется в стек вызовов. Вот как это выглядит:

В какой-то момент количество вызовов в стеке превышает размер стека, и браузер решает вмешаться, выбросив исключение, которое выглядит примерно так:

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

Одновременность и петля событий

Что произойдет, если в стеке есть вызов функции, требующей много времени на выполнение? Например, представьте, что вы хотите сделать какую-то сложную обработку изображения, используя JavaScript в браузере.

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

И это не все проблемы. При большом стеке, заполненном функциями, браузер может надолго задуматься. Большинство браузеров вмешиваются, выбрасывая ошибку, спрашивая, не хотите ли вы остановить процесс на странице?

Не самый хороший UX, а?

Так как мы можем выполнять тяжелый код без блокировки интерфейса и зависания браузера? Ну, решение — асинхронные обратные вызовы.

Прим. переводчика: в оригинальной статье в конце идет ссылка на сервис SessionStack. Так как этот перевод объединяет две статьи, я помещу ссылку на сервис в конце. Кроме того, в конце статьи автор обещает рассказать про асинхронные обратные вызовы во второй статье цикла, но по факту этого не делает. Рассказ об асинхронных вызовах и петле событий — тема четвертой статьи, которую я переведу чуть позже.

Как работает JavaScript: устройство V8 и пять советов по оптимизации кода

Чуть выше мы разобрались с тем, как работает стек вызовов, и какие проблемы могут с ним быть у неопытных разработчиков. Теперь мы поговорим о движке V8. Движок JavaScript — программа, интерпретирующая код JS. Он может быть реализован как стандартный интерпретатор, или как JIT-компилятор байткода.

Вот список популярных реализаций движка JavaScript:

  • V8 — движок с открытым кодом, написанный на C++ Гуглом.
  • Rhino — разработка Mozilla Foundation, написан на Java, код открыт.
  • SpiderMonkey — первый движок JavaScript, который когда-то использовался в Netscape Navigator, и теперь используется в Firefox.
  • JavaScriptCore — движок, используемый в Safari, также известен как Nitro, разработка Apple с открытым кодом.
  • KJS — движок, разработанный Гарри Портеном для браузера Konqueror
  • Chakra(JScript9) — Internet Explorer.
  • Chakra(JavaScript) — Microsoft Edge.
  • Nashorn — часть проекта OpenJDK, разработка Oracle с открытым кодом.
  • JerryScript — легковесный движок для IoT.

Зачем был создан V8

Движок V8, созданный Гуглом — разработка с открытым исходным кодом на C++. Движок используется в Google Chrome. В отличие от остальных движков, V8 используется также как рантайм Node.JS.

Изначально V8 был создан для улучшения производительности JavaScript внутри браузеров. В целях производительности V8 транслирует JavaScript в более эффективный машинный код вместо интерпретации. Он компилирует код в байткод на лету, используя JIT, аналогично некоторым современным движкам вроде SpiderMonkey и Rhino. Основная разница в том, что V8 не перегоняет в байткод любой промежуточный код.

Компиляторы V8

До версии 5.9, выпущенной в апреле 2017 года, движок использовал два компилятора:

  • full-codegen, простой и очень быстрый компилятор, производящий простой и относительно медленный байткод.
  • Crankshaft — более сложный JIT–компилятор, производящий сильно оптимизированный байткод.

В V8 также используется несколько потоков выполнения:

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

При первом выполнении кода V8 использует full-codegen, который напрямую транслирует код в байткод без какой-либо оптимизации. Это позволяет запуститься очень быстро. Заметим, что V8 не использует промежуточный байткод, что позволяет обойтись без интерпретатора.

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

Наконец, в дело вступает Crankshaft в отдельном потоке. Он транслирует абстрактное синтаксическое дерево в SSA (static single-assignment representation), называемое Hydrogen, и пытается оптимизировать полученный граф. Большинство оптимизаций происходят на этом уровне.

Прим. переводчика: я не нашел адекватной и развернутой статьи по SSA на русском языке. Если у вас нет проблем с английским, взгляните на материал по SSA «Static Single Assignment Book», в противном случае придется ограничиться статьей в Википедии.

Встраивание

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

Скрытый класс

JavaScript — прототипный язык, в нем нет классов и объектов, создаваемых клонированием. Кроме того, это динамический язык, так что свойства могут быть легко добавлены или удалены из объекта после его создания.

Большинство JavaScript–интерпретаторов используют структуры типа словарь, основанные на хеш-функциях, чтобы хранить свойства объектов в памяти. Такие структуры делают получение значений свойств более дорогим, чем в статических языках вроде Java или C#. В Java все свойства объектов определяются фиксированной схемой объекта (классом) до этапа компиляции, и эта схема не может быть изменена в процессе выполнения. В C# есть тип dynamic, но это тема отдельного разговора.

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

Такой подход невозможен в 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.

Пока у Point нет свойств, так что C0 пока пуст.

Как только первое выражение «this.x = x» выполнится внутри функции Point, V8 создаст второй скрытый класс C1, основанный на C0. С1 описывает то, где в памяти находится значение свойства x относительно указателя на объект. В нашем случае x сохранен по смещению 0, то есть при поиске в памяти объекта первое смещение будет соответствовать свойству x. Кроме того, V8 обновит класс C0 с помощью «классового перехода»: если к объекту Point добавится свойство x, следует опираться на скрытый класс C1. Нашему экземпляру объекта Point также соответствует класс C1.

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

Аналогичный процесс повторится на выражении «this.y = 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 использует еще один подход для оптимизации динамически типизированных языков, известный как встроенное кеширование. Он основан на наблюдении повторных вызовов одного и того же метода для одного и того же типа объекта. Подробное объяснение принципов работы можно прочесть в материале «Optimizing dynamic JavaScript with inline caches». Мы рассмотрим общие принципы встроенного кеширования, на случай, если у вас нет времени читать подробный разбор.

Так как это работает? V8 хранит типы объектов, передаваемых в качестве параметров для недавних вызовов, и использует эту информацию для предположения того, какой тип объекта будет использоваться для будущих вызовов. Если V8 сможет сделать точное предположение, он сможет пропустить процесс поиска свойств объекта, и вместо этого обратиться напрямую к скрытому классу.

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

Встроенное кеширование — причина того, почему так важно разделять скрытые классы между объектами одного типа. Если для двух однотипных объектов будут использованы разные скрытые классы, V8 не сможет применить встроенное кеширование даже между двумя объектами одного типа, поскольку их разные скрытые классы будут иметь разное смещение для свойств.

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

Компиляция в машинный код

Как только Hydrogen граф будет оптимизирован, Crankshaft преобразует его в низкоуровневое представление под названием Lithium. Большинство реализаций Lithium специфичны для конкретной архитектуры. Распределение регистров происходит на этом уровне.

В итоге Lithium компилируется в машинный код. Затем происходит операция под названием on-stack replacement, OSR. Прежде чем движок начнет компилировать и оптимизировать очевидно долгоиграющие методы, мы скорее всего уже запустим их. V8 не собирается забывать то, что он уже выполнял, и просто запустить оптимизированные версии. Вместо этого он трансформирует контекст, стек и регистры, так что мы можем переключиться на оптимизированную версию прямо в процессе выполнения. Это действительно сложная задача, учитывая то, что в числе прочих оптимизаций V8 уже встроил некоторое количество кода. Но V8 не единственный движок, способный на это.

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

Сборка мусора

Для сборки мусора V8 использует традиционный подход поколений и mark–and–sweep для очистки старого поколения. Фаза разметки предполагает приостановку выполнения кода. В целях контроля затрат на сборку и более плавной работы V8 использует постепенную разметку: вместо обхода всей кучи в попытках пометить каждый объект, он обходит часть кучи и продолжает нормальную работу. Следующая сборка начнется с места, где остановилась предыдущая. Это позволяет делать небольшие паузы в процессе работы. Как мы выяснили выше, фаза очистки происходит в отдельном потоке.

Ignition и TurboFan

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

С версии 5.9 full-codegen и Crankshaft больше не будут использоваться в V8, а разработчики движка постараются реализовать новые возможности JavaScript и соответствующие оптимизации. Это значит, что в целом V8 получит более простую и поддерживаемую архитектуру.

Бенчмарк новой версии

Эти улучшения — только начало. Ignition и TurboFan проложат путь для дальнейших оптимизаций, которые увеличат производительность JavaScript и уменьшат размер движка в Chrome и Node.JS.

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

Наконец, вот несколько советов, которые помогут вам писать хорошо оптимизированный JavaScript код. Вы не найдете ничего такого, чего бы мы не разобрали выше. Это просто обобщение уже известных вещей для вашего удобства:

  1. Порядок добавления свойств: объявляйте свойства в том же порядке для однотипных объектов, чтобы они переиспользовали одни и те же скрытые классы.
  2. Динамические свойства: добавление свойств к объекту во время выполнения заставляет движок построить новый скрытый класс и лишает все предыдущие скрытые классы уже готовой оптимизации. По возможности объявляйте все свойства в конструкторе.
  3. Методы: код, в котором один и тот же метод выполняется несколько раз подряд, быстрее того, где всегда выполняются разные методы. Причина — встроенное кеширование.
  4. Массивы: избегайте sparse-массивов, в которых ключи — не последовательные числа. Sparse-массивы, которые не содержат каждый элемент внутри — по сути хеш-таблицы. Доступ к элементам таких массивов обходится дороже. Кроме того, старайтесь не объявлять сразу большие массивы. Увеличение массивов по необходимости обойдется дешевле. И наконец, не удаляйте элементы из массивов, потому что это превратит их в sparse-массивы.
  5. Помеченные значения: V8 представляет объекты и целые числа 32-мя битами, и использует один бит для хранения типа: объект это (flag=1) или число (flag=0). Таким образом, целые числа представляются как SMI (SMall Integer), поскольку на самом деле занимают только 31 бит. Если число больше 31 бита, V8 упаковывает его, преобразуя в double, и создает новый объект, помещая число внутрь. Старайтесь укладывать знаковые целые числа в 31 бит, чтобы избежать больших расходов на упаковку-распаковку.

Прим. переводчика: пятый пункт в оригинале звучит как «tagged values». Я не смог до конца понять, почему у этого пункта такое название, когда речь идет просто о накладных расходах на автоупаковку. Думаю, что в итоге это не помешает понять смысл совета.

В SessionStack мы стараемся следовать этим практикам для написания хорошо оптимизированного кода. Причина в том, что при интеграции SessionStack в ваше работающее приложение сервис записывает все: изменения DOM, взаимодействие с пользователем, исключения, стектрейсы, неудачные сетевые запросы и отладочные сообщения. С SessionStack вы можете воспроизвести проблемы вашего приложения и увидеть своими глазами все, что произошло, глазами пользователя. И все это — без влияния на производительность вашего кода.

Если вам интересно, у нас есть бесплатный тарифный план, чтобы можно было опробовать сервис.

Ресурсы

  • https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P—dtDvwXXEeD0/pub
  • https://github.com/thlorenz/v8-perf
  • http://code.google.com/p/v8/wiki/UsingGit
  • http://mrale.ph/v8/resources.html
  • https://www.youtube.com/watch?v=UJPdhx5zTaw
  • https://www.youtube.com/watch?v=hWhMKalEicY

Это первые две из четырех статей цикла. Следующая статья — об устройстве памяти в JavaScript и борьбе с утечками памяти. Она выйдет в среду на этой неделе. Не переключайтесь!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *