Оптимізуємо продуктивність JavaScript для V8

Передмова

Найголовніша порада

Дуже важливо давати будь-які поради щодо продуктивності в контексті. Оптимізація часто стає нав'язливою звичкою, і глибоке занурення в нетрі може відволікати від важливіших речей. Потрібний цілісний погляд на продуктивність веб-додатку - перш ніж зосередитися на цих порадах з оптимізації, варто проаналізувати свій код інструментами на зразок PageSpeed ​​і спочатку досягти хорошого результату в цілому. Це допоможе уникнути передчасної оптимізації.

Найкраща стратегія, що веде до створення швидкого веб-додатку виглядає так:

Щоб дотримуватись цієї стратегії, важливо розуміти, як V8 оптимізує JS, уявляти, як все відбувається під час виконання. Також важливо володіти правильними інструментами. У своєму виступі Деніел присвятив більше часу інструментам розробника; у цій статті я здебільшого розглядаю особливості архітектури V8.

Приховані класи

Поки до p2 не було додано властивість ".z", p1 і p2 всередині компілятора мали один і той же прихований клас, і V8 міг використовувати один і той же машинний код оптимізований для обох об'єктів. Чим рідше ви мінятимете прихований клас, тим краще буде продуктивність.

  • Ініціалізуйте всі об'єкти в конструкторах, щоб вони якнайменше змінювалися надалі.
  • Завжди ініціалізуйте властивості об'єкта в тому самому порядку.

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

  • Намагайтеся використати 31-бітові цілі зі знаком скрізь, деце можливо.

V8 використовує два види внутрішнього уявлення масивів:

  • Справжні масиви для компактних наборів ключів.
  • Хеш-таблиці в інших випадках.

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

Набагато швидше буде так:

У першому прикладі індивідуальні присвоєння відбуваються послідовно, і в той момент, коли a[2] отримує значення, компілятор перетворює a масив розпакованих чисел з подвійною точністю, а коли a[3] ініціалізується нечисловим елементом, відбувається зворотне перетворення. У другому прикладі компілятор відразу вибере необхідний тип масиву.

  • Маленькі фіксовані масиви краще ініціалізувати, використовуючи літерал масиву.
  • Заповнюйте маленькі масиви (add() робить код поліморфним:

Оптимізуючий компілятор

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

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

Ви можете подивитися, що саме оптимізується у вашому коді, використовуючи автономну версію двигуна d8:

(імена оптимізованих функцій будуть виведені в stdout)

Не всіфункції можуть бути оптимізовані. Зокрема, компілятор, що оптимізує, пропускає будь-які функції, що містять блоки try/catch .

Якщо потрібно використати блок try/catch , поміщайте критичний до продуктивності код зовні. Приклад:

Можливо, у майбутньому ситуація зміниться, і ми зможемо компілювати блоки try/catch компілятором, що оптимізує. Ви можете подивитися, які функції ігноруються, вказавши опцію --trace-bailout при запуску d8:

Деоптимізація

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

  • Уникайте змін прихованих класів у оптимізованих функціях.

Ви можете подивитися, які функції піддаються деоптимізації, запустивши d8 з опцією --trace-deopt :

Інші інструменти V8

Перелічені вище функції можуть бути передані Google Chrome під час запуску:

У d8 також є профільувальник:

Семінуючий профільувальник d8 робить знімки кожну мілісекунду і пише в v8.log.

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