Skip to content

Latest commit

 

History

History
213 lines (162 loc) · 17.9 KB

File metadata and controls

213 lines (162 loc) · 17.9 KB

JIT (Just In Time компиляция)

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

Интерпретация — когда мы поставляем код, как есть. Интерпретатор переводит и выполняет программу строка за строкой.

link

1) Парсинг:

Парсинг превращает код в абстрактное синтаксическое дерево (AST).

link

AST — это отображение синтаксической структуры кода в виде дерева. На самом деле это удобно для программы, хотя и тяжело читать. С использованием AST во многих утилитах пишутся расширения, в том числе:

  • ESLint;
  • Babel;
  • Prettier;
  • Jscodeshift;

link

2) Интерпретатор => Оптимизирующий JIT-компилятор ( <= 2016):

Вместо интерпретатора появляется оптимизирующий JIT-компилятор, то есть Just-in-time компилятор. Ahead-of-time компиляторы работают до исполнения приложения, а JIT — во время. В вопросе оптимизации JIT-компилятор пытается угадать, как код будет исполняться, какие будут использоваться типы, и оптимизировать код так, чтобы он лучше работал.

link

Такая оптимизация называется спекулятивной, потому что она спекулирует на знаниях о том, что происходило с кодом раньше. То есть если 10 раз было вызвано что-то с типом number, компилятор думает, что так будет все время и оптимизирует под этот тип. Но если на вход попадает Boolean, происходит деоптимизация.

#Пр:

 const foo=(a, b) => a + b;
 foo (1, 2);
 foo (2, 3);

Сложили один раз, второй раз. Компилятор строит предсказание: «Это числа, у меня есть крутое решение для сложения чисел!» А вы пишете foo('WTF', 'JS'), и передаете в функцию строки — у нас же JavaScript, мы можем и строку с числом сложить.

В этот момент происходит !!! деоптимизация: !!!

link

3) JIT => JIT + Интерпретатор (Ignition) ( >= 2017):

Раньше в схеме интерпретатора Ignition не было. Google изначально говорили о том, что интерпретатор не нужен — JavaScript и так достаточно компактный и интерпретируемый — мы ничего не выиграем.

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

!!! При JIT компиляции достаточно много времени тратится на анализ и оптимизацию (ваш файл загрузился, и он пытается понять, что в нем написано) + Когда происходит деоптимизация, нужно снова исходный взять код, т.е. его надо где-то хранить. На это уходило много памяти.

Таким образом у интерпретатора было две задачи:

  • уменьшить накладные расходы на парсинг;
  • уменьшить потребление памяти.

Задачи были решены переходом на интерпретатор с байткодом.

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

link

Байт-код — это абстракция машинного кода. Компилировать байт-код в машинный код проще, если байт-код спроектирован с использованием той же вычислительной модели, которая применяется в физическом процессоре. Именно поэтому интерпретаторы часто являются регистровыми или стековыми машинами. (Байткод в Chrome — это регистровая машина с аккумулятором. В SpiderMonkey стековая машина, там все данные лежат на стеке, а регистров нет. Здесь они есть.)

link

Здесь также происходят оптимизации, например, dead code elimination. Если участок кода не будет вызван, он выкидывается и дальше не хранится. Если Ignition увидит сложение двух чисел, он их сложит и оставит в таком виде, чтобы не хранить лишнюю информацию. Только после этого получается байткод.

4) Холодные и горячие функции (Оптимизации и деоптимизации):

Холодные функции — это те, которые вызывались один раз или не вызывались совсем.
Горячие — это те, которые вызывались несколько раз. Сколько именно раз, сказать нельзя — в любой момент это могут переделать. Но в какой-то момент функция становятся горячей, и движок понимает, что ее надо оптимизировать.

Схема работы:

  1. Ignition (интерпретатор) собирает информацию. Он не только преобразует JavaScript в байткод, но еще и понимает, какие на вход пришли типы, какие функции стали горячими, и обо всем этом говорит компилятору.
  2. Происходит оптимизация.
  3. Компилятор исполняет код. Все работает хорошо, но тут прилетает тип, который он не ожидал, у него нет кода для работы с этим типом.
  4. Происходит деоптимизация. Компилятор обращается к интерпретатору Ignition за этим кодом.

link

5) Предотвращение деоптимизации:

1. Соблюдение мономорфности в функциях;

Это когда на вход вашей функции всегда приходят одни и те же типы. То есть если у вас все время приходит string, то не надо передавать туда boolean.

2. Hidden classes:

Скрытые классы есть во всех движках, не только в V8. Везде они называются по-разному, в терминах V8 это Map.
__
Map описывает структуру объектов, потому что в принципе в JavaScript типизация возможна только структурная, не номинальная. Мы можем описать, как выглядит наш объект, что за чем в нем идет.
__
При удалении/добавлении свойств объектов Hidden classes у объекта меняется, присваивается новый.
Поэтому следование таким принципам:

  • Open–closed principle (SOLID);
  • Information Expert (GRASP);
  • Don't use mixins;

Не только повышают надежность и читаемость, но и производительность!!!!

Пр:

  • Создаем объект.
  • Привязываем к нему скрытый класс, который говорит, что это объект типа Point.
  • Добавили поле x — новый скрытый класс, который говорит, что это объект типа Point, в котором первым идет значение x.
  • Добавили y — новый Hidden classes, в котором x, а потом y.
  • Создали еще один объект — происходит то же самое. То есть он так же привязывает то, что уже создано. В этот момент эти два объекта имеют одинаковый тип (через Hidden classes).
  • Когда во второй объект добавляется новое поле, у объекта появляется новый Hidden classes. Теперь для движка p1 и p2 это объекты разных классов, потому что у них разные структуры
  • Если передать куда-то первый объект, то, когда вы передадите туда же второй, произойдет деоптимизация. Первый ссылается на один скрытый класс, второй — на другой.

link

3. Inline Caches (ICs):

Движки JavaScript используют ICs для запоминания информации о том, где найти свойства объектов, чтобы уменьшить количество затратных поисков (поиск в самом объекте, если поля нет в объекте, возможно, оно есть в его прототипе. Может быть, это setter, getter и так далее. Все это нужно проверять).

Пр:

  • Если мы вызываем функцию первый раз, все хорошо, интерпретатор сделал оптимизацию
  • Для второго вызова сохраняется мономорфное состояние.
  • Вызываю функцию третий раз, передаем чуть-чуть другой объект {x:3, y:1}. Происходит деоптимизация, появляется if, мы переходим в полиморфное состояние. Теперь код, который исполняет эту функцию, знает — ей на вход могут прилететь два разных типа объектов.
  • Если мы несколько раз передаем разные объекты, он остается в полиморфном состоянии, добавляя новые if. Но в какой-то момент сдается и переходит в мегаморфное состояние, т.е. когда: «На вход прилетает слишком много разных типов — я не знаю, как это оптимизировать!»

link

*Сейчас допускается 4 полиморфных состояний, но завтра их может быть 8. Это решают разработчики движка. Нам лучше оставаться в мономорфном, в крайнем случае, в полиморфном состоянии. Переход между мономорфным и полиморфным состояниями дорогой, потому что нужно будет сходить в интерпретатор, получить код заново и заново его оптимизировать.

  1. Массивы:

В JavaScript, не считая специфичных Typed Arrays, есть один тип массива.
В движке V8 их 6:

1) [1, 2, 3, 4] // PACKED_SMI_ELEMENTS — просто упакованный массив small integer. Для него есть оптимизации.
2) [1.2, 2.3, 3.4, 4.6] // PACKED_DOUBLE_ELEMENTS — упакованный массив double элементов, для него тоже есть оптимизации, но более медленные.
3) [1, 2, 3, 4, 'X'] // PACKED_ELEMENTS — упакованный массив, в котором есть объекты, строки и все остальное. Для него тоже есть оптимизации.

Следующие три типа — это массивы того же типа, что первые три, но с дырками:

4) [1,, 2,, 3, 4] // HOLEY_SMI_ELEMENTS
5) [1.2,, 2,, 3, 4] // HOLEY_DOUBLE_ELEMENTS
6) [1,, 'X'] // HOLEY_ELEMENTS

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

На схеме все, что выше, быстрее оптимизируется. То есть все ваши нативные методы — map, reduce, sort — внутри хорошо оптимизированы. Но с каждым типом оптимизация становится хуже:

link

  1. Array-Like Object: — это объекты, которые похожи на массивы, потому что у них есть признак длины. Самые популярные Array-Like Object: arguments и document.querySelectorAII. У нас появился map — мы его выдрали из прототипа и вроде бы можем использовать. Но если ему на вход пришел не массив, никакой оптимизации не будет. Наш движок не умеет делать оптимизацию по объектам.

Что нужно сделать?

  • Олдскульный вариант — через slice.call() превратить в настоящий массив.
  • Современный вариант еще лучше: написать (...rest), получить чистый массив — не arguments — все прекрасно!

link

  1. Большие массивы:
Загадка: new Array(1000) vs array = []
Какой вариант лучше: создать сразу большой массив и в цикле заполнять его 1000 объектами, 
или создать пустой и заполнять постепенно?

Правильный ответ: зависит от.

В чем отличие?

  • Когда мы создаем массив первым способом и заполняем 1000 элементов, мы создаем 1000 дырок. Этот массив не будет оптимизирован. Но в него будет быстро писать.
  • Создавая массив по второму варианту, выделяется немного памяти, мы записываем, например, 60 элементов, выделяется еще немного памяти, и т.д.

!!! То есть в первом случае быстро пишем — медленно работаем; во втором медленно пишем — быстро работаем.

Links:

Execution process:

link