Как работает JavaScript
Ранее JavaScript предназначался для использования в веб-браузерах, однако ситуация изменилась с развитием Node. Мы знаем, как, где и когда его использовать. Но известно ли, что происходит за этими сценариями?
Даже если вы знаете это, то все равно сможете извлечь полезную информацию из данной статьи.
JavaScript — это высокоуровневый ЯП, а компьютер понимает только единицы и нули. Каким образом компьютер понимает написанный код? В этой статье мы рассмотрим ответ на один единственный вопрос: как работает JavaScript?
Движок JavaScript
Это главный герой, который отвечает за понимание компьютером JS-кода. Движок JavaScript принимает код и преобразует его в машиночитаемый язык. Он выполняет работу переводчика, преобразующего JS-код на понятный компьютеру язык. Как и JS, каждый ЯП также обладает движком, делающий написанный код понятным для компьютера.
У JavaScript есть множество различных движков, доступных для использования. На этой странице Википедии можно найти их список. Они также называются движками ECMAScript (подробнее об этом ниже).
Попробуем заглянуть внутрь движка, чтобы узнать, как преобразуются файлы JavaScript.
Что скрывает движок JavaScript
Движок — это язык, который разбивает код и переводит его. А V8 — это один из самых популярных движков JavaScript, который используется в Chrome и NodeJS и написан на низкоуровневом языке C++. Написать движок может кто угодно, что может привести к путанице.
Для поддержания контроля за этими механизмами был создан стандарт ECMA, который предоставляет характеристики написания движка и всех функций JavaScript. По этой причине в ECMAScript 6, 7, 8 и т. д. реализованы новые функции JavaScript, а движки обновлены для поддержки этих новых функций. Следовательно, необходимо проверять доступность расширенной функции JS в браузерах во время разработки.
Для примера возьмем движок V8, однако основные концепции остаются неизменными во всех движках.
Теперь рассмотрим с более технической точки зрения.
Так выглядит движок JS изнутри. Вводимый код проходит через следующие стадии:
- Парсер
- AST
- Интерпретатор выдает байт-код
- Профайлер
- Компилятор выдает оптимизированный код
Не волнуйтесь, подробности рассмотрим в течение нескольких минут.
Однако для начала разберем важное различие.
Интерпретатор и Компилятор
Есть два способа преобразования кода в машиночитаемый язык. Концепция, о которой мы поговорим, применима не только к JavaScript, но и к большинству ЯП, таких как Python, Java и т. д.
- Интерпретатор читает код построчно и сразу выполняет его.
- Компилятор читает весь код, выполняет оптимизации, а затем производит оптимизированный код.
Рассмотрим на примере.
В приведенном выше примере функция add , которая добавляет два числа и возвращает сумму, вызывается 1000 раз.
- При передаче этого файла интерпретатору, он читает его построчно и сразу выполняет функцию, пока цикл не закончится. Таким образом, он просто переводит код в то, что компьютер понимает на ходу.
- При передаче этого файла компилятору, он читает всю программу, выполняет анализ действий и производит оптимизированный код на языке, который понимает машина. Это как взять X (JS-файл) и создать Y (оптимизированный код, понятный машине). Если использовать интерпретатор для Y (оптимизированный код), результат будет таким же, как при интерпретации X (JS-код).
На изображении выше показано, что байт-код — это просто промежуточный код, который необходимо интерпретировать для обработки компьютером. Как интерпретатор, так и компилятор, преобразуют исходный код в машинный код, единственное отличие состоит в том, как выполняется это преобразование.
- Интерпретатор построчно преобразует исходный код в эквивалентный машинный код.
- Компилятор сразу преобразует весь исходный код в машинный код.
Плюсы и минусы интерпретатора и компилятора
- Преимущество интерпретатора заключается в немедленном выполнении кода без необходимости компиляции, что может быть полезно для запуска JS-файлов в браузере. Однако процесс замедляется при необходимости преобразования большего количества JS-кода. Вспомните маленький фрагмент кода, в котором функция вызывалась 1000 раз. В этом случае вывод остается прежним, даже если функция add была вызвана 1000 раз. Такие ситуации замедляют работу интерпретатора.
- В таких случаях компилятор может выполнить некоторые оптимизации, заменив цикл одним числом 2 (1 + 1 добавлялось каждый раз), поскольку он остается неизменным для всех 1000 итераций. Окончательный код, который выдает компилятор, оптимизирован и выполняется намного быстрее.
Таким образом, интерпретатор сразу начинает выполнение кода, но не выполняет оптимизацию. Компилятору требуется время для компиляции кода, однако он выдает более оптимизированный код.
Теперь вернемся к основной схеме движка JS.
Итак, зная плюсы и минусы компилятора и интерпретатора, можно использовать преимущества каждого. Здесь и появляется компилятор JIT (Just In Time). Он представляет собой комбинацию интерпретатора и компилятора, и большинство браузеров теперь реализуют эту функцию для повышения эффективности. Движок V8 также использует эту функцию.
В этом процессе:
- Парсер идентифицирует, анализирует и классифицирует различные части программы. Например, устанавливает, является ли элемент функцией, переменной и т.д. с помощью ключевых слов JavaScript.
- Затем AST (абстрактные синтаксические деревья) создают древовидную структуру на основе классификации парсера. В AST Explorer можно узнать о том, как строится дерево.
- Затем дерево передается интерпретатору, который создает байт-код. Как мы узнали ранее, байт-код не является кодом самого низкого уровня, однако его можно интерпретировать. На этой стадии браузер работает с доступным байт-кодом с помощью движка V8, чтобы пользователю не приходилось ждать.
- В то же время профайлер ищет оптимизации кода, а затем передает входные данные компилятору. Компилятор выдает оптимизированный код, в то время как байт-код временно используется для рендеринга в браузере. Как только компилятор создает оптимизированный код, временный байт-код полностью заменяется новым оптимизированным кодом.
- Таким образом используются лучшие качества интерпретатора и компилятора. Интерпретатор выполняет код, в то время как профайлер ищет оптимизацию, а компилятор создает оптимизированный код. Затем байт-код заменяется оптимизированным кодом, который является кодом более низкого уровня, таким как машинный код.
Это означает, что производительность будет постепенно улучшаться, не блокируя время выполнения.
Примечание по байт-коду
Как и в случае с машинным кодом, не все компьютеры понимают байт-код. Чтобы интерпретировать его на машиночитаемый язык, необходимо промежуточное ПО, такое как виртуальная машина, или движок (например, Javascript V8). По этой причине браузеры могут выполнять этот байт-код из интерпретатора во время вышеупомянутых 5-ти стадий с помощью движков JavaScript.
В результате возникает следующий вопрос:
Является ли JavaScript интерпретируемым языком?
Да, но не совсем. На ранних этапах JavaScript Брендан Айк создал движок JavaScript ‘SpiderMonkey’. У движка был интерпретатор, который говорил браузеру, что нужно делать. Сейчас есть не только интерпретаторы, но и компиляторы, а код не только интерпретируется, но и компилируется для оптимизации. Технически все зависит от реализации.
На каком языке написан JavaScript?
Если это язык сценариев, как следует из названия, он должен быть написан на языке более низкого уровня, верно? Например, как PHP написан на C на каком языке написан JavaScript?
6 ответов:
Javascript-это просто стандарт, более Формально известный как ECMAScript. Он может быть реализован на любом языке, как и любой стандарт.
движок Javascript Chrome, V8, написано на C++.
со страницы проекта:
V8 написан на C++ и используется в Google Chrome, браузере с открытым исходным кодом от Google.
V8 реализует ECMAScript, как указано в ECMA-262, 5-е издание, и работает на Windows (XP или новее), Mac OS X (10.5 или новее) и Linux системы, использующие процессоры IA-32, x64 или ARM.
движок JavaScript Firefox,SpiderMonkey (и теперь TraceMonkey) также написан на C++. И как сказал маэрик ниже,носорог написано на Java.
все ответы до сих пор верны, но так как это еще не было упомянуто, JavaScript может быть написан на JavaScript.
большинство интерпретаторов Javascript написаны на C / C++ (V8, Nitro и т. д.), Однако совместимый интерпретатор может быть написан на любом языке (Rhino→Java,переводчик→Javascript и т. д.).
на каком бы языке ни был написан интерпретатор javascript webbrowsers клиента:)
Javascript является реализацией ECMAScript стандартный, но нет единственного канонического интерпретатора, как вы видите с PHP.
большинство основных реализаций (автономных или как части веб-браузеров) там, как правило, в основном написаны на C или C++ по соображениям производительности, но это не всегда так. Rhino, движок, поддерживаемый Mozilla, написан на Java.
большинство реализаций Javascript показывают поведение, которое явно вызвано использованием указателей и передачей параметров byref, что обычно указывает на использование C или C++
Это явно заметно, например, когда вы разбираете многомерный массив в цикле с помощью промежуточного массива. они, как правило, ведут себя очень «странно», если вы не знакомы с указателями и передачей параметров byref (вам нужно сделать var hlp = new Array () каждый раз или он будет перезаписывать предыдущие значения, которые вы уже сохранили где-то еще)
Мне довольно любопытно, как реализация javascript, например, в Java, потому что я предполагаю, что в этом случае такое поведение будет совсем другим?
Движок JavaScript
До 2008-2009 гг. движок JavaScript (называемый также интерпретатор JavaScript и реализация JavaScript) реализовывался как интерпретатор, считывающий и исполняющий исходный код на JavaScript.
Первый движок JavaScript создан Бренданом Айхом в Netscape Communications для браузера Netscape Navigator. Движок получил кодовое имя SpiderMonkey и был реализован на языке программирования Си. Впоследствии он обновлялся и в отношении него была достигнута совместимость с третьей редакцией спецификации ECMA-262. Движок JavaScript Rhino написан преимущественно Норрисом Бойдом (англ. Norris Boyd (тоже из Netscape) и представляет собой реализацию JavaScript на языке программирования Java. Как и SpiderMonkey, Rhino совместим с третьей редакцией спецификации ECMA-262. С данной спецификацией также совместимы Nitro из Apple Safari, V8 из Google Chrome и TraceMonkey из Mozilla Firefox 3.5.
Наиболее распространённой средой исполнения JavaScript является браузер. Как правило браузеры предоставляют открытый API для создания объектов среды исполнения (англ. host objects ), предоставляющих возможность работы с DOM в JavaScript.
Другой распространненой средой исполнения JavaScript является веб-сервер. Веб-сервер, поддерживающий JavaScript, предоставляет объекты среды исполнения, представляющие HTTP-запрос и HTTP-ответ. Манипулируя этими объектами, программа на JavaScript может динамически генерировать страницы. Например, технология ASP для веб-сервера IIS позволяет реализовывать серверную часть в том числе и на языке программирования JScript (реализация JavaScript от компании Microsoft). Другой пример это веб-сервер Jaxer (англ.), предоставляющий помимо объектов традиционных для серверов еще и объекты традиционные для браузеров. Преимуществом такого подхода является то, что один и тот же код может быть разделён между сервером и клиентом.
Движки JavaScript
Основные движки JavaScript:
Mozilla
-
— самый первый движок JavaScript, созданный Бренданом Айхом в Netscape Communications. , разрабатываемый Mozilla Foundation движок JavaScript с открытым исходным кодом, полностью написанный на Java.
- Tamarin [en] .
-
— движок JavaScript с открытым исходным кодом, разрабатываемый датским отделением компании Google. Используется в браузерах на основе Chromium, а также в Maxthon 3.
Другие
- KJS (англ.) — ECMAScript/JavaScript-движок среды рабочего стола KDE, изначально разработанный Гарри Портеном (англ.) для браузера Konqueror
- Narcissus (англ.) — движок JavaScript с открытым исходным кодом, написанный Бренданом Айхом
- Tamarin от Adobe Systems
- Nitro (изначально SquirrelFish) — движок JavaScript в Safari 4
- Chakra в Internet Explorer 9[1] . от Opera Software, используется в Opera, начиная с версии 10.50.
Эволюция производительности
В Firefox 3.5 (выпущен 30 июня 2009 года) используется техника оптимизации, предполагающая «в некоторых случаях улучшение производительности в 20-40 раз» [2] .
2 июня 2008 года команда разработчиков WebKit представила SquirrelFish [3] — новый движок JavaScript, в котором достигалось значительное улучшение скорости интерпретирования скриптов браузером Safari [4] . Этот движок был одной из новых возможностей Safari 4. Тестовая версия появилась 11 июня 2008 года; в итоге движок был переименован в Nitro.
С тех пор началась гонка разработчиков браузеров по увеличению скорости движков JavaScript. С 2008 года пальму первенства в ней удерживает Google Chrome: это подтверждает множество независимых экспериментов [5] [6] [7] . С появлением Squirrelfish Extreme от разработчиков WebKit и Tracemonkey от Mozilla, производительность JavaScript в Google Chrome перестала расцениваться как самая высокая [8] [9] [10] [11] . Однако датское подразделение Google разработало движок JavaScript V8, со значительно увеличенной производительностью JavaScript в Google Chrome 2.
Как правило, браузер имеет браузерный движок, занимающийся отрисовкой страниц, и движок JavaScript, что упрощает тестирование, переиспользование или использование в других проектах. Например, Caracan используется с Presto, Nitro с WebKit, Rhino и SpiderMonkey с Gecko, KJS с KHTML, по умолчанию, ни с одним из браузерных движков не используется. Иногда возможны другие комбинации, например, V8 с WebKit в Google Chrome. Движок JavaScript позволяет разработчикам получить доступ к функциональности (работа с сетью, с DOM, с внешними событиями, с HTML5 video, canvas, storage), необходимой для управлением веб-браузером.
Sunspider — инструмент тестирования производительности браузера (англ.), используемый для измерения производительности движков JavaScript в более чем дюжине тестов, каждый из которых заточен на отдельную часть языка JavaScript. Sunspider не предназначен для тестирования возможностей, связанных с чем-то помимо вычислений (HTML, CSS, работа с сетью).
Реализации
JavaScript — диалект ECMAScript, поддерживаемый многими приложениями, в частности, браузерами. Диалекты иногда включают расширения языка или стандартную библиотеку и соответствующие API , такие как Document Object Model от W3C. Это означает, что приложения, написанные для одного диалекта могут быть несовместимы с другим, если только они не используют общее подмножество поддерживаемых возможностей и API.
Нет чёткой разницы между диалектом и реализацией. Диалект языка — это вариация языка с существенными особенностями в то время как реализация языка/диалекта выполняет программу, написанную на этом диалекте.
Как работает JS: о внутреннем устройстве V8 и оптимизации кода
Перед вами — второй материал из серии, посвящённой особенностям работы JavaScript на примере движка V8. В первом шла речь о механизмах времени выполнения V8 и о стеке вызовов. Сегодня мы углубимся в особенности V8, благодаря которым исходный код на JS превращается в исполняемую программу, и поделимся советами по оптимизации кода.
О JS-движках
JavaScript-движок — это программа, или, другими словами, интерпретатор, выполняющий код, написанный на JavaScript. Движок может быть реализован с использованием различных подходов: в виде обычного интерпретатора, в виде динамического компилятора (или JIT-компилятора), который, перед выполнением программы, преобразует исходный код на JS в байт-код некоего формата.
Вот список популярных реализаций JavaScript-движков.
-
— движок с открытым исходным кодом, написан на C++, его разработкой занимается Google.
— этот движок с открытым кодом поддерживает Mozilla Foundation, он полностью написан на Java.
— это самый первый из появившихся JS-движков, который в прошлом применялся в браузере Netscape Navigator, а сегодня — в Firefox.
— ещё один движок с открытым кодом, известный как Nitro и разрабатываемый Apple для браузера Safari.
— JS-движок KDE, который разработал Гарри Портен для браузера Konqueror, входящего в проект KDE.
— движок для Internet Explorer.
— движок для Microsoft Edge.
— движок с открытым кодом, являющийся частью OpenJDK, которым занимается Oracle.
— легковесный движок для интернета вещей.
Почему был создан движок 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, за исключением того, что создаются они во время выполнения программы. Посмотрим, как всё это работает, на следующем примере:
Когда происходит вызов 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
Переходы между скрытыми классами зависят от порядка, в котором к объекту добавляются свойства. Взгляните на этот пример кода:
В подобной ситуации можно предположить, что у объектов 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-кода можете поделиться вы?