Чим корисний мономорфізм?



Виступи і пости в блогах про продуктивність JavaScript часто звертають увагу на важливість мономорфного коду, проте зазвичай не дається ніякого виразного пояснення, що таке мономорфізм/поліморфізм і чому це має значення. Навіть мої власні виступи найчастіше зводяться до дихотомії в стилі Неймовірного Халка: «ОДИН ТИП ДОБРЕ! ДВА ТИП ПОГАНО!». Не дивно, що коли люди звертаються до мене за порадою по продуктивності, найчастіше вони просять пояснити, що насправді таке мономорфізм, звідки береться поліморфізм і що в ньому поганого.

Ситуацію ускладнює ще й те, що саме слово «поліморфізм» має безліч значень. У класичному об'єктно-орієнтованому програмуванні поліморфізм пов'язаний зі створенням дочірніх класів, в яких можна змінити поведінку базового класу. Програмісти, які працюють з Haskell, замість цього подумають про параметричному поліморфізм. Однак поліморфізм, про який попереджають в доповідях про продуктивність JavaScript – це поліморфізм викликів функції.

Я пояснював цей механізм стількома різними шляхами, що нарешті зібрався і написав цю статтю: тепер можна буде не імпровізувати, а просто дати посилання на неї.

Я також спробував новий спосіб пояснювати речі – зображуючи взаємодію складових частин віртуальної машини у вигляді коротких коміксів. Крім того, дана стаття не покриває деякі деталі, які я порахував незначними, зайвими або не пов'язаними безпосередньо.

Динамічний пошук для чайників

Задля простоти викладу ми будемо розглядати переважно самі елементарні звернення до властивостей в JavaScript, як наприклад
o.x
у коді нижче. У той же час важливо розуміти, що все, про що ми говоримо, відноситься до будь-якої операції з динамічним зв'язуванням, будь то звернення до властивості або арифметичний оператор, і застосовується не тільки в JavaScript.

function f(o) {
return o.x
}

f({ x: 1 })
f({ x: 2 })

Уявіть на хвилину, що ви собеседуетесь на відмінну посаду в ТОВ «Інтерпретатори», і вам пропонують придумати і реалізувати механізм звернення до властивості у віртуальній машині JavaScript. Яка відповідь була б самим банальним і простим?

Складно придумати щось простіше, ніж взяти готову семантику JS специфікації ECMAScript (ECMA 262) і переписати алгоритм [[Get]] слово в слово з англійської мови на C++, Java, Rust, Malbolge або будь-який інший мову, який ви вважали за краще використовувати для співбесіди.

Взагалі кажучи, якщо відкрити випадковий інтерпретатор JS, швидше за все можна побачити щось на зразок:

jsvalue Get(jsvalue self, jsvalue property_name) {
// 8.12.3 тут реалізація методу [[Get]]
}

void Interpret(jsbytecodes bc) {
// ...
while (/* залишилися необроблені байткоды */) {
switch (op) {
// ...
case OP_GETPROP: {
jsvalue property_name = pop();
jsvalue receiver = pop();
push(Get(receiver, property_name));
// TODO(mraleph): викинути виняток у strict mode, як сказано в 8.7.1 крок 3.
break;
}
// ...
}
}
}

Це абсолютно правильний спосіб реалізувати звернення до властивості, однак у нього є один значний недолік: у порівнянні з сучасними віртуальними машинами JS наша реалізація дуже повільна.

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


От якби можна було отримати інформацію про тих об'єктах, які ми вже бачили, і застосувати її до схожих об'єктів! В теорії це дозволило б нам заощадити багато часу, не використовуючи загальний алгоритм, а найбільш підходящий для об'єктів конкретної форми.

Ми знаємо, що пошук властивості у випадковому об'єкті — трудомісткий операція, тому добре б зробити це один раз, а знайдений шлях закешувати, використовуючи форму об'єкта в якості ключа. Коли ми в наступний раз зустрінемо об'єкт тієї ж форми, можна буде отримати шлях з кеша набагато швидше, ніж обчислювати його з нуля.

Ці техніка оптимізації називається інлайн-кешування та я вже писав про неї раніше. Для даної статті не будемо вдаватися в деталі реалізації, а звернемо увагу на дві важливі речі, про які я раніше замовчував: як і будь-який інший кеш, інлайн-кеш має розмір (кількість закешированных елементів в даний момент) і ємність (максимальну кількість елементів, яке може бути закешировано).

Повернемося до прикладу:

function f(o) {
return o.x
}

f({ x: 1 })
f({ x: 2 })

Скільки записів у інлайн-кеші для значення
o.x
?

Оскільки
{x: 1}
та
{x: 2}
мають однакову форму (вона іноді називається прихований клас), відповідь — 1. Саме це стан кеша ми називаємо мономорфным, тому що нам траплялися об'єкти тільки однієї форми.

Моно («один») + морф («форма»)



А що трапиться, якщо ми викличемо функцію
f
з об'єктом іншої форми?

f({ x: 3 })
// кеш o.x все ще мономорфен
f({ x: 3, y: 1 })
// а тепер?

Об'єкти
{x: 3}
та
{x: 3, y: 1}
мають різну форму, тому наш кеш тепер містить два записи: одну для
{x: *}
і одну для
{x *, y: *}
. Операція стала поліморфної з рівнем поліморфізму, рівним двом.

Якщо ми продовжимо передавати в
f
об'єкти різних форм, рівень поліморфізму буде рости, поки не досягне межі ємності для інлайн-кеша (наприклад, у V8 для звернення до властивостей межа ємності буде дорівнює чотирьом). Після цього кеш перейде в мегаморфное стан.

f({ x: 4, y: 1 }) // поліморфізм, рівень 2
f({ x: 5, z: 1 }) // поліморфізм, рівень 3
f({ x: 6, a: 1 }) // поліморфізм, рівень 4
f({ x: 7, b: 1 }) // мегаморфизм


Мегаморфное стан потрібно для того, щоб не дати кешу безконтрольно рости, і по суті означає «Тут занадто багато форм, відстежувати їх далі марно». У віртуальній машині V8 кеші в мегаморфном стані можуть все одно продовжувати щось кешувати, але вже не локально, а в глобальну хеш-таблицю. Вона має фіксований розмір, і при колізії значення перезаписуються.

Невелика вправа для перевірки розуміння:

function ff(b, o) {
if (b) {
return o.x
} else {
return o.x
}
}

ff(true, { x: 1 })
ff(false, { x: 2, y: 0 })
ff(true, { x: 1 })
ff(false, { x: 2, y: 0 })

  1. Скільки інлайн-кешей звернення до властивості оголошені функції
    ff
    ?
  2. В якому вони стані?
ВідповідіКешей два і обидва мономорфны, оскільки кожен з них потрапляли тільки об'єкти однієї форми.


Вплив на продуктивність

На даному етапі продуктивність різних станів інлайн-кеша стають очевидними:

  • Мономорфное — найшвидше, якщо ви кожен раз звертаєтеся до значення з кеша (ОДИН ТИП ДОБРЕ!)
  • Поліморфний — лінійний пошук серед значень кеша
  • Мегаморфное — звернення до глобальної хеш-таблиці. Самий повільний варіант з кешированных, але все одно швидше, ніж промах кеша
  • Промах кеша — звернення до поля, якого немає в кеші. Доводиться платити за перехід в рантайм і використання загального алгоритму
Насправді, це тільки половина правди: крім прямого прискорення коду, інлайн-кеші також працюють шпигунами на службі у всемогутнього оптимізуючого компілятора, який рано чи пізно прийде і прискорить ваш код ще більше.

Спекуляції та оптимізації
Інлайн-кеші не можуть забезпечити максимальну продуктивність поодинці з двох причин:

  • Кожен інлайн-кеш працює незалежно і нічого не знає про сусідів
  • Кожен інлайн-кеш може промахнутися і тоді доведеться використовувати рантайм: в кінцевому рахунку це узагальнена операція з узагальненими побічними ефектами, і навіть тип значення, що повертається часто невідомий
function g(o) {
return o.x * o.x + o.y * o.y
}

g({x 0, y: 0})

У прикладі вище інлайн-кешей аж сім штук: два на
.x
, два на
.y
, ще два на
*
і один на
+
. Кожен діє самостійно, перевіряючи об'єкт
o
на відповідність закешированной формі. Арифметичний кеш оператора
+
перевірить, чи є обидва операнда числами, хоча це можна було б вивести з станів кешей операторів
*
. Крім того, в V8 є декілька внутрішніх уявлень для чисел, так що це теж буде враховано.

Деякі арифметичні операції в JS мають притаманний їм конкретний тип, наприклад
a | 0
завжди повертає 32-бітове ціле, а
+a
— просто число, але для більшості інших операцій таких гарантій немає. З цієї причини написання AOT-компілятора для JS є неймовірно складним завданням. Замість того, щоб компілювати JS-код заздалегідь один раз, більшість віртуальних машин JS має кілька режимів виконання.

Наприклад, V8 спочатку збирає всі звичайним компілятором і відразу ж починає виконувати. Пізніше часто використовувані функції перекомпілюються із застосуванням оптимізацій. З іншого боку, Asm.js використовує неявні типи операцій і з їх допомогою описує дуже строго обмежена підмножина Javascript зі статичною типізацією. Такий код можна оптимізувати ще до його запуску, без спекуляцій адаптивної компіляції.

«Прогрів» коду потрібен для двох цілей:

  • Зменшує затримку при старті: оптимізуючий компілятор включається тільки в тому випадку, якщо код був використаний достатню кількість разів
  • Інлайн-кеші встигають зібрати інформацію про форми об'єктів
Як вже було сказано вище, написаний людьми код на JavaScript зазвичай містить недостатньо інформації для того, щоб його можна було повністю статично типізувати і провести AOT-компіляцію. JIT-компілятора доводиться будувати здогади про те, як поводиться ваша програма, і видавати оптимізований код, який підходить тільки при певних припущеннях. Іншими словами, йому потрібно знати, які об'єкти використовуються в оптимізованної функції. За щасливим збігом обставин, це саме та інформація, яку збирають інлайн-кеші!

  • Мономорфный кеш говорить: «Я бачив тільки тип A»
  • Поліморфний кеш говорить: «Я бачив тільки типи А1, ..., AN»
  • Мегаморфный кеш каже: «Та я багато чого бачив...»

Оптимізуючий компілятор аналізує інформацію, зібрану інлайн-кешами, і по ній будує проміжне представлення (IR). Інструкції IR зазвичай більш специфічні і низькорівневі, ніж операції в JS. Наприклад, якщо кеш для звернення до властивості
.x
бачив тільки об'єкти форми
{x, y}
, тоді компілятор може згенерувати IR-інструкцію, яка отримує значення за фіксованим зсувом від покажчика на об'єкт. Зрозуміло, використовувати таку оптимізацію для будь-якого приходить об'єкта небезпечно, тому компілятор також додасть перевірку типу перед нею. Перевірка типу порівнює форму об'єкта з очікуваною, і якщо вони відрізняються — оптимізований код виконувати не можна. Замість цього викликається неоптимізований код і виконання продовжується звідти. Цей процес називається деоптимизацией. Розбіжність типу — не єдина причина, по якій деоптимизация може статися. Вона також може статися, якщо арифметична операція була «заточена» під 32-бітні числа, а результат попередньої операції спричинив переповнення, або при зверненні до неіснуючій індексу масиву
arr[idx]
(вихід за межі діапазону, розріджений масив з пропусками і т. д.).


Стає зрозуміло, що оптимізація потрібна для усунення описаних вище недоліків:

Неоптимізований код Оптимізований код
Кожна операція може мати невідомі побічні ефекти і реалізує повну семантику Спеціалізація коду усуває або обмежує невизначеність, побічні ефекти суворо відомі (наприклад, звернення до властивості по зміщенню їх немає)
Кожна операція сама по собі, навчається самостійно і не ділиться досвідом із сусідами Операції розкладаються на низькорівневі IR-інструкції, які оптимізуються разом, що дозволяє усунути надлишковість

Зрозуміло, побудова IR під конкретні форми об'єктів — це тільки перший крок в ланцюжку оптимізації. Коли проміжне представлення сформовано, компілятор буде пробегаться по ньому кілька разів, виявляючи інваріанти і вирізаючи зайве. Цей тип аналізу зазвичай обмежений областю процедури і компілятори змушений враховувати найгірші з можливих побічних ефектів на кожному виклику. Слід пам'ятати, що виклик може ховатися в будь неконкретизированной операції: наприклад, оператор
+
може викликати
valueOf
, а звернення до властивості викличе його getter-метод. Таким чином, якщо операцію не вийшло конкретизувати на першому етапі, про неї будуть спотикатися всі наступні проходи оптимізатора.

Один з найбільш поширених видів надлишкових інструкцій — це постійні перевірки на те, що один і той ж об'єкт має одну і ту ж форму. Ось так початкове проміжне представлення може виглядати для функції
g
з прикладу вище:

CheckMap v0, {x,y} ;; shape check 
v1 Load v0, @12 ;; load o.x 
CheckMap v0, {x,y} 
v2 Load v0, @12 ;; load o.x 
i3 Mul v1, v2 ;; o.x * o.x 
CheckMap v0, {x,y} 
v4 Load v0, @16 ;; load o.y 
CheckMap v0, {x,y} 
v5 Load v0, @16 ;; load o.y 
i6 Mul v4, v5 ;; o.y * o.y 
i7 Add i3, i6 ;; o.x * o.x + o.y * o.y

Тут форма об'єкта в змінній
v0
перевіряється 4 рази, хоча між перевірками немає операцій, які могли б викликати її зміна. Уважний читач також помітить, що завантаження
v2
та
v5
також надлишкова, оскільки ніякої код їх не перезаписує. На щастя, наступний прохід глобальної нумерації значень видалить ці інструкції:

;; Після ГНЗ 
CheckMap v0, {x,y} 
v1 Load v0, @12 
i3 Mul v1, v1 
v4 Load v0, @16 
i6 Mul v4, v4 
i7 Add i3, i6 

Як вже було сказано вище, прибрати ці інструкції стало можливо тільки тому, що між ними не було операцій з побічними ефектами. Якби між завантаженнями
v1
та
v2
був виклик, нам довелося б припускати, що він може змінити форму об'єкта
v0
, а отже перевірка форми
v2
була б обов'язковою.

Якщо ж операція не мономорфна, компілятор не може просто взяти ту ж саму зв'язку «перевірка форми та конкретизированная операція», як в прикладах вище. Інлайн-кеш містить інформацію про декількох формах. Якщо взяти будь-яку з них і зав'язатися тільки на неї, то всі інші будуть призводити до деоптимизации, що небажано. Замість цього компілятор буде будувати дерево рішень. Наприклад, якщо інлайн-кеш звернення до властивості
o.x
зустрічав тільки форми A, B і C, розгорнута структура буде аналогічна наступного (це псевдокод, насправді буде побудований граф потоку управління):

var o_x
if ($GetShape(o) === A) {
o_x = $LoadByOffset(o, offset_A_x)
} else if ($GetShape(o) === B) {
o_x = $LoadByOffset(o, offset_B_x)
} else if ($GetShape(o) === C) {
o_x = $LoadByOffset(o, offset_C_x)
} else {
// o.x потрапляли лише A, B, C
// тому ми припускаємо, що нічого* іншого бути не може
$Deoptimize()
}
// Тут ми знаємо тільки те, що має одну o
// з форм - A, B або C. Але ми вже не знаємо,
// яку саме

У мономорфных звернень є одна дуже корисна властивість, якого немає у поліморфних: до наступної операції з побічним ефектом існує гарантія того, що форма об'єкта залишилася незмінною. Поліморфний звернення дає тільки слабку гарантію «об'єкт має одну з декількох форм», за допомогою якої мало що можна оптимізувати (в кращому випадку, можна було б прибрати останнє порівняння і блок з деоптимизацией, але V8 так не надходить).

З іншого боку, V8 може сформувати ефективне проміжне представлення, якщо поле розташовується за однаковим зміщення у всіх формах. У цьому випадку буде використано поліморфна перевірка форми:

// Перевіряємо, що форма є однією з A, B або C - інакше деоптимизируем
$TypeGuard(o, [A, B, C])
// Завантажуємо властивість, оскільки у всіх трьох формах зміщення однакову
var o_x = $LoadByOffset(o, offset_x)

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

Коли інлайн-кеш передає компілятору інформацію про можливі форми об'єкта, компілятор може порахувати, що робити для кожної з них по розгалуженню у графі недоцільно. В такому випадку буде згенерований трохи інший граф, де в кінці буде не деоптимизация, а узагальнена операція:

var o_x
if ($GetShape(o) === A) {
o_x = $LoadByOffset(o, offset_A_x)
} else if ($GetShape(o) === B) {
o_x = $LoadByOffset(o, offset_B_x)
} else if ($GetShape(o) === C) {
o_x = $LoadByOffset(o, offset_C_x)
} else {
// Ми знаємо, що звернення до o.x мегаморфно.
// Щоб уникнути деоптимизации залишимо лазівку
// для випадкових об'єктів:
o_x = $LoadPropertyGeneric(o, 'x')
// ^^^^^^^^^^^^^^^^^^^^ невідомі побічні ефекти
}
// В цей момент про форму об'єкта ""
// більше нічого не відомо, і могли
// відбутися побічні ефекти

У деяких випадках компілятор може взагалі відмовитися від затії конкретизувати операції:

  • Немає способу ефективно її конкретизувати
  • Операція полиморфна і компілятор не знає, як побудувати дерево рішень (таке відбувалося з поліморфним зверненням до
    arr[i]
    , але вже виправлено)
  • Немає інформації про тип, яким операція може бути конкретизована (код жодного разу не виконувався, збирач сміття видалив зібрану інформацію про форми і т. д.)
У цих (досить рідкісних) випадках компілятор видає узагальнений варіант проміжного представлення.

Вплив на продуктивність

У підсумку маємо наступне:

  • Мономорфние операції оптимізуються легше за все і дають оптимізатору простір для дій. Як сказав би Халк — "ОДИН ТИП БЛИЗЬКО ДО ЗАЛОЗУ!"
  • Операції з невеликим рівнем поліморфізму, що вимагають перевірку форми, або дерево рішень, повільніше мономорфных:
    • Дерева рішень ускладнюють потік виконання. Компілятор складніше поширювати інформацію про типах і прибирати зайві інструкції. Умовні переходи дерева також можуть запороти продуктивність, якщо потраплять у навантажений цикл.
    • Поліморфні перевірки форм не так сильно перешкоджають оптимізації і все ще дозволяють видаляти деякі зайві інструкції, але все одно повільніше мономорфных. Вплив на продуктивність залежить від того, як добре ваш процесор працює з умовними переходами.

  • Мегаморфные або высокополиморфные операції не конкретизуються взагалі, і в проміжному поданні виклик, з усіма витікаючими наслідками для оптимізації і продуктивності CPU.
Примітка перекладача: автор давав посилання на микробенчмарк, однак сам сайт більше не працює.

Необсужденное
Я навмисне не став вдаватися в деякі деталі реалізації, щоб стаття не стала занадто великою.

Форми

Ми не обговорювали, як форми (або приховані класи) влаштовані, як вони обчислюються і присвоюються об'єктам. Загальне уявлення можна отримати з моєї попередній статті або виступу на AWP2014.

Потрібно тільки пам'ятати, що саме поняття «форми» у віртуальних машинах JS — це евристичне наближення. Навіть якщо з точки зору людини у двох об'єктів однакова форма, з точки зору машини вона може бути різною:

function A() { this.x = 1 }
function B() { this.x = 1 }

var a = new A,
b = new B,
c = { x: 1 },
d = { x: 1; y: 1 }

delete d.y
// a, b, c, d - чотири різних форми для V8

Оскільки об'єкти в JS безтурботно реалізовані у вигляді словників, можна застосувати поліморфізм по випадковості:

function A() {
this.x = 1;
}

var a = new A(), b = new A(); // форма однакова

if (something) {
a.y = 2; // форма a більше не збігається з формою b
}

Умисний поліморфізм

У багатьох мовах, де форма створеного об'єкта не може бути змінена (Java, C#, Dart, C++ тощо), поліморфізм також підтримується. Можливість написати код, заснований на інтерфейсах і виконуються в залежності від конкретної реалізації — дуже важливий механізм абстракції. Статично типізованих мовах поліморфізм впливає на продуктивність аналогічним чином.

Не дивно, що JVM використовує інлайн-кеші для оптимізації інструкцій
invokeinterface
та
invokevirtual
.

Не всі кеші однаково корисні

Слід мати на увазі, що деякі кеші засновані не на формах об'єктів, та\або мають меншу ємність у порівнянні з іншими. Наприклад, інлайн-кеш виклику функції може мати тільки три стани: неинициализированное, мономорфное або мегаморфное. Проміжного поліморфного стану у нього немає, тому що для виклику функції форма об'єкту не має значення, а кешується сам об'єкт функції.

function inv(cb) {
return cb(0)
}

function F(v) { return v }
function G(v) { return v + 1 }

inv(F)
// інлайн-кеш мономорфный, вказує на F
inv(G)
// інлайн-кеш стає мегаморфным

Якщо функція
inv
оптимізована і інлайн-кеш знаходиться в мономорфном стані, оптимізатор може вбудувати тіло функції в місце виклику (що особливо важливо для коротких функцій в популярних місцях). Якщо ж кеш в мегаморфном стані, оптимізатор не знає, тіло який саме опції можна вбудовувати, і тому залишить узагальнений оператор виклику в проміжному поданні.

З іншого боку, виклики методів виду
o.m(...)
працює аналогічно звернення до властивості. Інлайн-кеш виклику методу також може мати проміжне поліморфний стан. Віртуальна машина V8 може вбудувати виклик такої функції в мономорфном, полиморфном, або навіть мегаморфном вигляді тим же способом, як з властивістю: спочатку перевірка типу або дерево рішень, після якого розташовується саме тіло функції. Є тільки одне обмеження: метод повинен міститися у формі об'єкта.

насправді,
o.m(...)
використовує відразу два інлайн-кеша — один для завантаження властивості, інший безпосередньо для виклику функції. Другий має тільки два стани, як у прикладі вище у функції
c
. Тому його стан ігнорується при оптимізації виклику і використовується тільки стан інлайн-кеша звернення до властивості.


function inv(o) {
return o.cb(0)
}

var f = {
cb: function F(v) { return v },
};

var g = {
cb: function G(v) { return v + 1 },
};

inv(f)
inv(f)
// інлайн-кеш в мономорфном стані,
// бачив тільки об'єкти типу f
inv(g)
// а тепер в полиморфном, тому що 
// крім f бачив ще і g

Може здатися несподіваним, що в прикладі вище
f
та
g
мають різну форму. Так відбувається тому, що коли ми присвоюємо властивості об'єкта функцію, V8 намагається (якщо можливо) прив'язати її до форми об'єкта, а не до самого об'єкта. У прикладі вище
f
має форму
{c: F}
, то сама форма посилається на замикання. У всіх прикладах до цього форми містили тільки ознака наявності певної властивості — в даному ж випадку зберігається і його значення, а форма стає схожа на клас з мов зразок Java або C++.

Звичайно, якщо потім перезаписати властивість іншою функцією, V8 вважатиме, що це більше не схоже на взаємовідношення класу і методу, тому форма зміниться:

var f = {
cb: function F(v) { return v },
};

// Форма f дорівнює {cb: F}

f.cb = function H(v) { return v - 1 }

// Форма f дорівнює {cb: *}

Про те, як V8 будує і підтримує форми об'єктів, варто було б написати окрему велику статтю.

Подання шляху до властивості

До цього моменту створюється враження, що інлайн-кеш звернення до властивості
o.x
— це такий словник, сопоставляющий форми та зміщення всередині класу, як
Dictionary<Shape, int>
. Насправді таке уявлення не передає глибину: властивість може належати одному з об'єктів в ланцюжку прототипів, або ж мати методи доступу. Очевидно, що властивості з геттерами-сеттерами вважаються менш конкретизованими, ніж звичайне властивості з даними.

Наприклад, об'єкт
o = {x: 1}
можна представити як об'єкт, властивість якого завантажує і зберігає значення в спеціальний прихований слот з допомогою методів віртуальної машини:

// псевдокод, який описує o = { x: 1 }
var o = {
get x () {
return $LoadByOffset(this, offset_of_x)
},
set x (value) {
$StoreByOffset(this, offset_of_x, value)
}
// геттер/сетер згенеровані віртуальною машиною
// і невидимі для звичайного JS-коду
};
$StoreByOffset(o, offset_of_x, 1)

до Речі, приблизно так реалізовані властивості Dart VM

Якщо дивитися так, то інлайн-кеш повинен бути скоріше представлений у вигляді
Dictionary<Shape, Function>
— зіставляючи форми з функціями-аксессорами. На відміну від наївного уявлення зі зміщенням, так можна описати і властивості з ланцюжка прототипів, і геттери-сетери, і навіть проксі-об'єкти з ES6.

Премономорфное стан

Деякі інлайн-кешы в V8 мають ще один стан, що називається премономорфным. Воно служить для того, щоб не компілювати код для кешей, до яких зверталися всього один раз. Я не став згадувати про це стан, оскільки воно є маловідомою особливістю реалізації.

Фінальний рада по продуктивності
Найкращий порада щодо продуктивності можна взяти з назви книги Дейла Карнегі «Як перестати турбуватися і почати жити».

І правда, переживати за поліморфізму зазвичай безглуздо. Замість цього проженіть ваш код з нормальними даними, пошукайте вузькі місця профайлером — і якщо вони пов'язані з JS, то варто поглянути на проміжне представлення, яке виробляє компілятор.

І тільки тоді, якщо в середині вашого навантаженого циклу ви побачите інструкцію під назвою
XYZGeneric
або що-то буде позначено атрибутом
changes [*]
(тобто «змінює все»), тоді (і тільки тоді) можна починати хвилюватися.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.