Секрети швидкодії JavaScript: V8 та приховані класи

7 хв. читання

Сьогодні JavaScript стала однією з найбільш використовуваних мов для веброзробки. Та щоб піднятися на цей щабель, їй потрібно було подолати багато перешкод. Однією з них була швидкість виконання: і зараз JS успішно досягла продуктивності рівня C++.

Це було б неможливо, якби не з'явився рушій JavaScript V8.

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

Що таке V8 і як він працює

V8 — це рушій JavaScript з відкритим кодом, створений Google. Він написаний на C++ і підтримує Google Chrome, Chromium-браузери та NodeJS. Відповідає V8 за взаємодію з середовищем та генерування байткоду для запуску програм.

Спочатку V8 був представлений як механізм пришвидшення браузерів, але з часом він став значно досконалішим інтерпретатором, ніж будь-який інший рушій.

Найпомітніша відмінність між V8 та іншими рушіями — його компілятор Just In Time (JIT). Компілятор JIT компілює весь JavaScript у машинний код під час виконання і не генерує проміжного коду.

Секрети швидкодії JavaScript: V8 та приховані класи
Високорівнева архітектура рушія V8

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

Але всі ми знаємо, що компілятори значно швидші за інтерпретатори. Тоді чому рушій V8 використовує інтерпретатор замість компілятора?

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

Однак інтерпретатор Ignition відповідає лише за перший запуск вашого коду. Потім згенерований байт-код буде використовуватися компілятором під назвою Turbofan. Він оптимізує ваш код на основі даних, які отримуються під час виконання коду, і перекомпілює його в оптимізовану версію.

Примітка: Хоча V8 застосовується для оптимізації JavaScript, він написаний на C++ і застосовує багатопотоковий підхід для одночасного керування всією цією роботою.

У поясненні роботи V8 ми згадали, що інтерпретатор Ignition бере вхідним абстрактне синтаксичне дерево, тож розгляньмо, що це таке і як воно допомагає V8 поліпшити швидкодію JavaScript.

Абстрактне синтаксичне дерево

Абстрактні синтаксичні дерева застосовуються для побудови абстрактної структури програмного коду компілятора. Вони працюють не лише з JavaScript або V8; майже кожна мова програмування використовує AST для перетворення тверджень високорівневого коду у твердження низькорівневого коду.

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

Для кращого розуміння візьмемо простий код JavaScript та створимо для нього AST:

// Декларування функції
function addition(x, y){
   var answer = x + y;
   console.log(answer);
}// Виклик функції
addition(10,20);

Потім застосуємо інструмент онлайн-парсингу від esprima, щоб створити AST для цього коду. Ось цей фрагмент коду показує частину AST (повну версію можете переглянути тут).

{
 "type": "Program",
 "body": [
   {
     "type": "FunctionDeclaration",
     "id": {
       "type": "Identifier",
       "name": "addition"
     },
     "params": [
      {
       "type": "Identifier",
       "name": "x"
      },
      {
       "type": "Identifier",
       "name": "y"
      }
     ],
     "body": {
       "type": "BlockStatement",
       "body": [        ...       ],
      "kind": "var"
     },   ... "sourceType": "script"
}

AST визначає пари ключових значень для кожного рядка коду. Ідентифікатор початкового типу визначає, що AST належить програмі, а потім усі рядки коду визначаються всередині тіла, що є масивом об'єктів.

Як ми згадували, усі оголошення функцій, оголошення змінних, назви та типи впорядковуються по рядках, а коментарі не враховуються.

Крім процесу оптимізації та застосування AST, V8 використовує ще одну хитрість для поліпшення швидкодії JavaScript. Подивимось, що це таке і як воно працює.

Приховані класи для оптимізації коду JavaScript

Як ми знаємо, JavaScript — це мова динамічних типів. Тобто ми можемо додавати або вилучати атрибути з об'єктів на льоту.

Секрети швидкодії JavaScript: V8 та приховані класи

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

Як працюють приховані класи

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

Візьмімо вже наведений приклад і подивімося, як генеруються приховані класи:

Отже, коли ми створюємо порожній об'єкт (const userObject = {}), V8 створює відповідний прихований клас (C01) без будь-яких зміщень (offsets).

Секрети швидкодії JavaScript: V8 та приховані класи

Потім змінюємо цей об'єкт, додавши нову властивість (userObject.name = "Chameera"). Тепер рушій V8 створить новий прихований клас (C02), який успадковує всі властивості попереднього прихованого класу (C01) і призначить атрибут name до offset 0.

Секрети швидкодії JavaScript: V8 та приховані класи

Це дозволить компілятору обійти пошук у словнику під час звернення до назви властивості, а V8 вказуватиме безпосередньо на клас C01.

Якщо додати до цього об'єкта іншу властивість, відбудеться той самий процес. Створиться ще один прихований клас, котрий матиме й попередні, й нові атрибути offset.

Секрети швидкодії JavaScript: V8 та приховані класи

Ця концепція прихованого класу не лише дозволяє обійти пошук у словнику; з нею можна повторно застосовувати вже сформовані класи під час створення або зміни схожих об'єктів.

Наприклад, якщо ви створите інший порожній об'єкт під назвою article (const articleObject = {}), рушій V8 не створить новий прихований клас. Натомість він вказуватиме на вже створений клас C01.

Але якщо ви зміните articleObject, додавши нову властивість під назвою articleName, то V8 не зможе застосовувати раніше створений клас (C02), оскільки він має лише властивість під назвою name.

Написання високошвидкісного коду JavaScript

Отже, якщо ви хочете пришвидшити код JavaScript, можливо, вам доведеться скоротити динамічне додавання властивостей.

Припустимо, ви запускаєте цикл у NodeJS. Якщо додати динамічні властивості до об'єкта, ви побачите різницю швидкодії всередині циклу. Тому краще створювати властивості поза циклом і користуватися ними, а не додавати їх динамічно всередині циклу.

Ось чому V8 працює значно краще, коли повторно використовує наявні приховані класи.

Висновок

Коли ми обговорюємо роботу JavaScript, то згадуємо про цикли подій, мікрозавдання, макрозавдання та черги зворотних викликів. Однак усі вони не реалізовані у JavaScript. Натомість вони є частиною рушія V8 і відповідають за оптимізацію JavaScript-коду.

Саме тому ми й обговорили, як працює V8 та як він використовує концепцію прихованого класу для оптимізації вашого коду. Сподіваємось, це буде для вас корисно 🙂.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.2K
Приєднався: 9 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація