April 23

Оптимизация JavaScript. Inline Caches

Думаю, ни для кого не секрет, что все популярные JavaScript движки имеют схожий пайплайн выполнения кода. Выглядит он примерно следующим образом. Интерпретатор быстро компилирует JS-код в байткод "на лету". Полученный байткод начинает исполняться и параллельно обрабатывается оптимизатором. Оптимизатору требуется время на такую обработку, но в итоге может получиться высоко-оптимизированный код, который будет работать в разы быстрее. В движке V8 роль интерпретатора выполняет Ignition, а оптимизатором является Turbofan. В движке Chakra, который используется в Edge, вместо одного оптимизатора целых два, SimpleJIT и FullJIT. А в JSC (Safari), аж три Baseline, DFG и FTL. Конкретная реализация в разных движка может отличаться, но принципиальная схема, в целом одинакова.

Сегодня мы будем говорить об одном из множества механизмов оптимизации, который называется Inline Caches.

Итак, давайте возьмем самую обычную функцию и попробуем понять, как она будет работать в "runtime".

function getX(obj) {
  return obj.x;
}

Наша функция довольно примитивна. Простой геттер, который в качестве аргумента принимает объект, а на выходе возвращает значение свойства x этого объекта. С точки зрения JS-разработчика функция выглядит абсолютно атомарно. Однако, давайте заглянем под капот движка и посмотрим, что она представляет из себя в скомпилированном виде.

Для экспериментов, как обычно, будем использовать самый популярный JS-движок V8 (на момент написания статьи версия 12.6.72). Для полноценного дебага, кроме тела самой функции нам нужна её вызвать с реальным аргументом. Также, добавим вывод информации об объекте, он понадобится нам чуть ниже.

function getX(obj) {
  %DebugPrint(obj);
  return obj.x;
}

getX({ x: 1 });

Напомню, %DebugPrint является строенной функцией V8, чтобы использовать её в коде, нужно запустить рантайм d8 с флагом --allow-natives-syntax. Долнительно, выведем в консоль байткод исполняемого скрипта (флаг --print-bytecode).

%> d8 --print-bytecode --allow-natives-syntax index.js 
...
// байткод функции getX
[generated bytecode for function: getX (0x0b75000d9c29 <SharedFunctionInfo getX>)]
Bytecode length: 10
Parameter count 2
Register count 0
Frame size 0
         0x28c500002194 @    0 : 65 af 01 03 01    CallRuntime [DebugPrint], a0-a0
         0x28c500002199 @    5 : 2d 03 00 00       GetNamedProperty a0, [0], [0]
         0x28c50000219d @    9 : ab                Return
Constant pool (size = 1)
0xb75000d9dcd: [FixedArray] in OldSpace
 - map: 0x0b7500000565 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0x0b7500002b91 <String[1]: #x>
Handler Table (size = 0)
Source Position Table (size = 0)
// Далее информация о переданном объекте
DebugPrint: 0xb75001c9475: [JS_OBJECT_TYPE]
 - map: 0x0b75000d9d81 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0b75000c4b11 <Object map = 0xb75000c414d>
 - elements: 0x0b75000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0b75000006cd <FixedArray[0]>
 - All own properties (excluding elements): {
    0xb7500002b91: [String] in ReadOnlySpace: #x: 1 (const data field 0), location: in-object
 }
0xb75000d9d81: [Map] in OldSpace
 - map: 0x0b75000c3c29 <MetaMap (0x0b75000c3c79 <NativeContext[285]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x0b75000d9d59 <Map[16](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0b7500000a31 <Cell value= 1>
 - instance descriptors (own) #1: 0x0b75001c9485 <DescriptorArray[1]>
 - prototype: 0x0b75000c4b11 <Object map = 0xb75000c414d>
 - constructor: 0x0b75000c4655 <JSFunction Object (sfi = 0xb7500335385)>
 - dependent code: 0x0b75000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

Итак, в сущности, скомпилированный байткод функции getX выглядит следующим образом.

0x28c500002194 @    0 : 65 af 01 03 01    CallRuntime [DebugPrint], a0-a0
0x28c500002199 @    5 : 2d 03 00 00       GetNamedProperty a0, [0], [0]
0x28c50000219d @    9 : ab                Return

Первая строчка, это вызов функции %DebugPrint. Она носит исключительно вспомогательный характер и на исполняемый код не влияет, мы можем её спокойно опустить. Следующая инструкция GetNamedProperty. Её задача - получить значение указанного свойства из переданного объекта. На вход инструкция получает три параметра. Первый - ссылка на объект. В нашем случае, ссылка на объект хранится в a0 - первом аргументе функции getX. Второй и третий параметры, это адрес скрытого класса и offset нужно свойства в массиве дескрипторов.

Форма объекта

Что такое скрытые классы, массив дескрипторов и как вообще устроены объекты в JavaScript я подробно рассказывал в статье Структура объекта в JavaScript движках. Не буду пересказывать всю статью, обозначу только нужные нам факты. Каждый объект имеет один или несколько скрытых классов (Hidden Classes). Скрытые классы являются исключительно внутренним механизмом движка и JS-разработчику не доступны, ну, по крайней мере, без применения специальных встроенных методов движка. Скрытый класс представляет, так называемую, форму объекта и всю необходимую информацию о свойствах объекта. Сам же объект хранит только значения свойств и ссылку на скрытый класс. Если два объекта имеют одинаковую форму, у них будет ссылка на один и тот же скрытый класс.

const obj1 = { x: 1 }
const obj2 = { x: 2 }

//          {}     <- empty map
//          |
//        { x }    <- common map
//       /    \
// <obj1>      <obj2>

По мере добавления свойств объекту, его дерево скрытых классов увеличивается.

const obj1 = {}
obj1.x = 1;
obj1.y = 2;
obj1.z = 3;

// {} -> { x } -> { x, y } -> { x, y, z } -> <obj1>

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

function getX(obj) {
  return obj.x;
}

getX({ y: 1, x: 2, z: 3 });

Что получить значение свойства x, интерпретатор должен знать: а) адрес последнего скрытого класса объекта; б) offset этого свойства в массиве десктрипторов.

d8> const obj = { y: 1, x: 2, z: 3};
d8>
d8> %DebugPrint(obj);
DebugPrint: 0x2034001c9435: [JS_OBJECT_TYPE]
 - map: 0x2034000d9cf9 <Map[24](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2034000c4b11 <Object map = 0x2034000c414d>
 - elements: 0x2034000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x2034000006cd <FixedArray[0]>
 - All own properties (excluding elements): {
    0x203400002ba1: [String] in ReadOnlySpace: #y: 1 (const data field 0), location: in-object
    0x203400002b91: [String] in ReadOnlySpace: #x: 2 (const data field 1), location: in-object
    0x203400002bb1: [String] in ReadOnlySpace: #z: 3 (const data field 2), location: in-object
 }
0x2034000d9cf9: [Map] in OldSpace
 - map: 0x2034000c3c29 <MetaMap (0x2034000c3c79 <NativeContext[285]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x2034000d9cd1 <Map[24](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x203400000a31 <Cell value= 1>
 - instance descriptors (own) #3: 0x2034001c9491 <DescriptorArray[3]>
 - prototype: 0x2034000c4b11 <Object map = 0x2034000c414d>
 - constructor: 0x2034000c4655 <JSFunction Object (sfi = 0x203400335385)>
 - dependent code: 0x2034000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

В примере выше скрытый класс расположен по адресу 0x2034000d9cd1 (back pointer).

d8> %DebugPrintPtr(0x2034000d9cd1)
DebugPrint: 0x2034000d9cd1: [Map] in OldSpace
 - map: 0x2034000c3c29 <MetaMap (0x2034000c3c79 <NativeContext[285]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - unused property fields: 1
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x2034000d9c89 <Map[24](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x203400000a31 <Cell value= 1>
 - instance descriptors #2: 0x2034001c9491 <DescriptorArray[3]>
 - transitions #1: 0x2034000d9cf9 <Map[24](HOLEY_ELEMENTS)>
     0x203400002bb1: [String] in ReadOnlySpace: #z: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x2034000d9cf9 <Map[24](HOLEY_ELEMENTS)>
 - prototype: 0x2034000c4b11 <Object map = 0x2034000c414d>
 - constructor: 0x2034000c4655 <JSFunction Object (sfi = 0x203400335385)>
 - dependent code: 0x2034000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
0x2034000c3c29: [MetaMap] in OldSpace
 - map: 0x2034000c3c29 <MetaMap (0x2034000c3c79 <NativeContext[285]>)>
 - type: MAP_TYPE
 - instance size: 40
 - native_context: 0x2034000c3c79 <NativeContext[285]>

Из которого мы можем получить массив дескрипторов по адресу 0x2034001c9491 (instance descriptors).

d8> %DebugPrintPtr(0x2034001c9491)
DebugPrint: 0x2034001c9491: [DescriptorArray]
 - map: 0x20340000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
 - enum_cache: 3
   - keys: 0x2034000daaa9 <FixedArray[3]>
   - indices: 0x2034000daabd <FixedArray[3]>
 - nof slack descriptors: 0
 - nof descriptors: 3
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0x203400002ba1: [String] in ReadOnlySpace: #y (const data field 0:s, p: 2, attrs: [WEC]) @ Any
  [1]: 0x203400002b91: [String] in ReadOnlySpace: #x (const data field 1:s, p: 1, attrs: [WEC]) @ Any
  [2]: 0x203400002bb1: [String] in ReadOnlySpace: #z (const data field 2:s, p: 0, attrs: [WEC]) @ Any
0x20340000062d: [Map] in ReadOnlySpace
 - map: 0x2034000004c5 <MetaMap (0x20340000007d <null>)>
 - type: DESCRIPTOR_ARRAY_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x203400000061 <undefined>
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x203400000701 <DescriptorArray[0]>
 - prototype: 0x20340000007d <null>
 - constructor: 0x20340000007d <null>
 - dependent code: 0x2034000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

Где интерпретатор может найти нужное свойство по его имени и получить offset, в нашем случае равный 1.

Таким образом, путь интерпретатора будет выглядеть примерно следующим образом.

Наш случай довольно простой. Здесь значения свойств хранятся внутри самого объекта (in-object), что делает доступ к ним довольно быстрым. Но, тем не менее, определение позиции свойства в массиве дескрипторов все равно будет пропорционально количеству самих свойств. Более того, в статье Структура объекта в JavaScript движках я говорил, что значения не всегда хранятся таким "быстрым" образом. В ряде случаев тип хранилища может быть изменен на медленный "словарь".

Inline Cache

Конечно, в единичных случаях для JS-движка это все не вызывает больших трудностей, а время доступа к значению свойств вряд ли можно заметить невооруженным глазом. Но что, если наш случай не единичный? Допустим, нам требуется выполнить функцию сотни или тысячи раз в цикле? Суммарное время работы тут приобретает критическую важность. При том, что функция, фактически выполняет одни и те же операции, разработчики JavaScript предложили оптимизировать процесс поиска свойства и не выполнять его каждый раз. Все, что для этого требуется, сохранить адрес скрытого класса объекта и offset нужного свойства.

Вернемся к самому началу статьи и к инструкции GetNamedProperty, которая имеет три параметра. Мы уже выяснили, что первый параметр - это ссылка на объект. Второй параметр - адрес скрытого класса. Третий параметр - найденный offset свойства. Определив эти параметры единожды, функция может их запомнить и при следующем запуске не выполнять процедуру поиска значения снова. Такое кэширование и называется Inline Cache (IC).

TurboFan

Однако, стоит учитывать, что мемоизация параметров тоже требует памяти и некоторого процессорного времени. Это делает механизм эффективным только при интенсивном выполнении функции (так называемая, hot-функция). Насколько интенсивно выполняется функция зависит от количества и частоты её вызовов. Оценкой интенсивности занимается оптимизатор. В случае с V8 в роли оптимизатора выступает TurboFan. Прежде всего, оптимизатор собирает граф из AST и байткода и отправляет его на фазу "inlining", где собираются метрики вызовов, загрузок и сохранений в кэш. Далее, если функция пригодна для inline-кэширования, она встает в очередь на оптимизацию. После того как очередь заполнена оптимизатор должен определить самую "горячую" из них и произвести кэширование. Потом взять следующую и так далее. В отличие, кстати, от его предшественника Crankshaft, который брал функции по очереди начиная с первой. Вообще, данная тема довольно большая и заслуживает отдельной статьи, сейчас рассматривать все подробности и особенности эвристики TurboFan смысла не имеет . Перейдем лучше к примерам.

Типы IC

Чтобы проанализировать работу IC, нужно включить логирование в runtime-среде. В случае с d8 это можно сделать, указав флаг --log-ic.

%> d8 --log-ic

function getX(obj) {
  return obj.x;
}

for (let i = 0; i < 10; i++) {
  getX({ x: i });
}

Как я уже говорил, TurboFan начинает кэшировать свойства объектов только в hot-функциях. Чтобы сделать нашу функцию таковой, нам потребуется запустить её в цикле несколько раз подряд. В моем простом скрипте в среде d8 понадобилось минимум 10 итераций. На практике, в других условиях и при наличии других функций, это число может варьироваться и, скорее всего, будет больше. Полученный лог теперь можно прогнать через System Analyzer.

Grouped by type: 1#
>100.00%	1	LoadIC
Grouped by category: 1#
>100.00%	1	Load
Grouped by functionName: 1#
>100.00%	1	~getX
Grouped by script: 1#
>100.00%	1	Script(3): index.js
Grouped by sourcePosition: 1#
>100.00%	1	index.js:3:14
Grouped by code: 1#
>100.00%	1	Code(JS~)
Grouped by state: 1#
>100.00%	1	0 → 1
Grouped by key: 1#
>100.00%	1	x
Grouped by map: 1#
>100.00%	1	Map(0x3cd2000d9e61)
Grouped by reason: 1#
>100.00%	1	

В списке IC List мы видим нотацию типа LoadIC, которая говорит о получении доступа к свойству объекта из кэша. Здесь же указана сама функция functionName: ~getX, адрес скрытого класса Map(0x3cd2000d9e61) и название свойства key: x. Но наибольший интерес представляет state: 0 -> 1.

Существует несколько типов IC. Полный список выглядит следующим образом:

/src/common/globals.h#1481

// State for inline cache call sites. Aliased as IC::State.
enum class InlineCacheState {
  // No feedback will be collected.
  NO_FEEDBACK,
  // Has never been executed.
  UNINITIALIZED,
  // Has been executed and only one receiver type has been seen.
  MONOMORPHIC,
  // Check failed due to prototype (or map deprecation).
  RECOMPUTE_HANDLER,
  // Multiple receiver types have been seen.
  POLYMORPHIC,
  // Many DOM receiver types have been seen for the same accessor.
  MEGADOM,
  // Many receiver types have been seen.
  MEGAMORPHIC,
  // A generic handler is installed and no extra typefeedback is recorded.
  GENERIC,
};

В системном анализаторе эти типы обозначаются символами

0: UNINITIALIZED
X: NO_FEEDBACK
1: MONOMORPHIC
^: RECOMPUTE_HANDLER
P: POLYMORPHIC
N: MEGAMORPHIC
D: MEGADOM
G: GENERIC

Например, тип X (NO_FEEDBACK) говорит о тои, что оптимизатор пока не собрал достаточной статистики, чтобы поставить функцию на оптимизацию. В нашем же случае мы видим state: 0 -> 1 означает, что функция перешла в состояние "мономорфного IC".

Monomorphic

Я уже говорил, что на практике, одна и та же функция может принимать объект в качестве аргумента. Форма этого объекта на конкретном вызове функции может отличаться от предыдущего. Что несколько усложняет жизнь оптимизатору. Меньше всего проблем возникает в случае, когда мы передаем в функцию только одинаковый по форме объекты, как в нашем последнем примере.

function getX(obj) {
  return obj.x; // Monomorphic IC
}

for (let i = 0; i < 10; i++) {
  getX({ x: i }); // все объекты имеют одинаковую форму
}

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

Polymorphic

Давайте теперь попробуем добавить вызов функции с объектами разной формы.

function getX(obj) {
  return obj.x; // Polymorphic IC
}

for (let i = 0; i < 10; i++) {
  getX({ x: i, y: 0 });
  getX({ y: 0, x: i });
}

В "IC List" теперь мы видим:

50.00%        1    0 → 1
50.00%        1    1 → P

Функция в рантайме получила несколько разных форм объекта. Доступ к свойству x у каждой формы будет отличаться. У каждой свой адрес скрытого класса и свой offset этого свойства. В этом, случае оптимизатор выделяет по два слота для каждой формы объекта, куда и сохраняет нужные параметры. Условно представить такую структура можно в виде массива наборов параметров.

[
  [Map1, offset1],
  [Map2, offset2],
  ...
]

Такой тип IC называется полиморфным. У полиморфного IC есть ограничение на количество допустимых форм.

/src/wasm/wasm-constants.h#192

// Maximum number of call targets tracked per call.
constexpr int kMaxPolymorphism = 4;

По умолчанию, полиморфный тип может от 2 до 4 форм на функцию. Но этот параметр можно регулировать флагом --max-valid-polymorphic-map-count.

Megamorphic

Если же форм объекта больше, чем может переварит Polymorphic, тип меняется на мегаморфный.

function getX(obj) {
  return obj.x; // Megamorphic IC
}

for (let i = 0; i < 10; i++) {
  getX({ x: i });
  getX({ prop1: 0, x: i });
  getX({ prop1: 0, prop2: 1, x: i });
  getX({ prop1: 0, prop2: 1, prop3: 2, x: i });
  getX({ prop1: 0, prop2: 1, prop3: 2, prop4: 3, x: i });
}

Результат IC List:

40.00%        2    P → P
20.00%        1    0 → 1
20.00%        1    1 → P
20.00%        1    P → N

Поиск нужного набора параметров в таком случае нивелирует экономию процессорного времени и, следовательно, не имеет никакого смысла. Поэтому оптимизатор просто сохраняет в кэш символ MegamorphicSymbol. Для следующих вызовов фукнции это будет означать, что закэшированных параметров тут нет, их придется брать обычным способом. Равно как и нет смысла дальше ставить функцию на оптимизацию и собирать её метрики.

Заключение

Вы наверняка заметили, что в списке типов IC присутствует еще MEGADOM. Этот тип используется для кэширования нод DOM-дерева. Дело в том, что механизм инлайн-кэширования не ограничивается только функциями и объектами. Он активно применяется и во многих других местах, в том числе и за пределами V8.Ввесь объем информации о кэшировании за один раз мы физически покрыть не сможем. А раз уж мы сегодня говорим об объектах JavaScript, то и тип MegaDom рассматривать в этой статье не будем.

Давайте проведем пару тестов и посмотрим, на сколько эффективно работает оптимизация Turbofan в V8. Эксперимент будем ставить в среде Node.js (последняя стабильная версия на момент написания статьи v20.12.2).

Экспериментальный код:

const N = 1000;

//===

function getXMonomorphic(obj) {
  let sum = 0;

  for (let i = 0; i < N; i++) {
    sum += obj.x;
  }

  return sum;
}

console.time('Monomorphic');

for (let i = 0; i < N; i++) {
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
}

console.timeLog('Monomorphic');

//===

function getXPolymorphic(obj) {
  let sum = 0;

  for (let i = 0; i < N; i++) {
    sum += obj.x;
  }

  return sum;
}

console.time('Polymorphic');

for (let i = 0; i < N; i++) {
  getXPolymorphic({ x: i, y: 0 });
  getXPolymorphic({ y: 0, x: i });
  getXPolymorphic({ x: i, y: 0 });
  getXPolymorphic({ y: 0, x: i });
  getXPolymorphic({ x: i, y: 0 });
}

console.timeEnd('Polymorphic');

//===

function getXMegamorphic(obj) {
  let sum = 0;

  for (let i = 0; i < N; i++) {
    sum += obj.x;
  }

  return sum;
}

//===

console.time('Megamorphic');

for (let i = 0; i < N; i++) {
  getXMegamorphic({ x: i });
  getXMegamorphic({ prop1: 0, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, prop3: 2, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, prop3: 2, prop4: 3, x: i });
}

console.timeLog('Megamorphic');

Для начала запустим скрипт с отключенной оптимизацией и посмотрим "чистую" скорость работы функций.

%> node --no-opt test.js
Monomorphic: 68.55ms
Polymorphic: 69.939ms
Megamorphic: 85.045ms

Для чистоты эксперимента я повторил тесты несколько раз. Скорость работы мономорфных и полиморфных объектов примерно одинакова. Полиморфные иногда могу оказываться даже быстрее. Связано это уже не столько с работой самого V8, сколько с системными ресурсами. А вот скорость мегаморфных объектов несколько ниже в силу того, что на этом тапе образуется дерево скрытых классов и доступ к свойствам этих объектов априори сложнее, чем в первых двух случаях.

Теперь запустим тот же скрипт с включенной оптимизацией:

%> node test.js
Monomorphic: 9.313ms
Polymorphic: 9.673ms
Megamorphic: 29.104ms

Скорость работы мономорфных и полиморфных функций увеличилась примерно в 7 раз. Как и в предыдущем случае, разница между этими двумя типами незначительна и при повторных испытаниях полиморфные иногда бывают даже быстрее. А вот скорость мегаморфной функции увеличилась всего в 3 раза. Вообще, исходя из теории, описанной выше, мегаморфные функции не должны были показать прирост скорости на оптимизации. Однако, не все так просто. Во-первых, у них все же имеется побочный эффект от оптимизации, поскольку такие функции исключаются из процесса дальнейшего сбора метрик. Это хоть и не большое, но все же преимущество перед остальными типами. Во-вторых, оптимизация работы JS не ограничена инлайн-кэшированием доступа к свойствам объекта. Существует еще ряд других видов оптимизаций, которые в этой статье мы не рассматривали.

Более того, в 2023-м году Google выпустил релиз "Chromium M117", в который был включен новый оптимизатор Maglev. Он был встроен как промежуточное звено между Ignition (интерпретатором) и Turbofan (оптимизатором). Теперь процесс оптимизации приобрел трех-ступенчатую архитектуру и выглядит как Ignition -> Maglev -> Turbofan. Maglev вносит свой вклад в оптимизацию функций, в частности, он работает с аргументами функции. Подробнее об этом поговорим в другой раз. А пока можем сделать вывод, что мегаморфные функции примерно в два раза медленнее мономорфных и полиморфных.


Мои телеграмм-каналы:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

English version: https://blog.frontend-almanac.com/js-optimisation-ic