система онлайн-бронирования
г. Донецк, Украина, ул. Артёма, 87
+38 (062) 332 33 32, 332-27-71
ЗАБРОНИРОВАТЬ
НОМЕР

Статьи

jsunderhood digest (in Russian)

  1. Питання / Відповіді
  2. Q: Як можна подивитися оптимізований код в Рантайм?
  3. Q: Як влаштований оптимізатор в V8.
  4. Q: Як і чи потрібно прогрівати функції вручну?
  5. Q: Майбутнє JS оптимізацій
  6. Q: V8 vs. arguments object
  7. Q: Продуктивність apply, call, bind
  8. Q: V8 vs. forEach
  9. Q: defineProperty vs V8
  10. Пастка hidden class transition collision
  11. Загадки про продуктивність
  12. Шпаргалка: уявлення рядків в V8.
  13. Загадка №2: на Stack Oveflow

Я провів тиждень відповідаючи на питання через твіттер аккаунт @jsunderhood. Це коротке резюме тижні, найцікавіші її технічні моменти.

Питання / Відповіді

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

Q: Як можна подивитися оптимізований код в Рантайм?

Зовсім вже в Рантайм не можна (поки що?), Але можна змусити V8 виплюнути дещо на консоль або в окремі файли. Для цього у V8 існує ряд прапорів:

  • --trace-hydrogen доступний навіть в будь-який збірці V8 і дозволяє подивитися високорівневе представлення (high-level IR), що використовується компілятором з оптимізацією;
  • --print-code і --print-opt-code доступні в збірках V8 з включеним дизассемблером і дозволяють подивитися згенерований нативний код;

На сьогоднішній день найпростіше дивитися на все це за допомогою IRHydra - це тул, який я написав сам для себе, як раз для того, щоб було легко вивчати, як оптимізується-деоптімізіруется конкретний шматок JS коду. Всі інструкції на першій сторінці, всі баги слід надсилати сюди .

Q: Як влаштований оптимізатор в V8.

V8 класичний представник спекулятивного адаптивного мультіуровнего оптимізатора (speculative adaptive multi-tier optimizer). Приблизна схема виглядає ось так

Спочатку весь код компілюється швидким неоптімізірующім компілятором. Причому ця компіляція лінива - більшість функцій компілюються тільки тоді, коли вони викликані перший раз. Швидкий компілятор нічого не оптимізує, він просто фарширує генерований код вбудованими кешами (inline cache). Ці кеші дозволяють навіть неоптимізованими коду виконуватися відносно швидко.

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

Тут важливо зауважити, що оптимізатор оперує на рівні окремо взятої функції, яку йому дали оптимізувати, і здебільшого сліпий за її межами. Висловлюючись класичними термінами, цей оптимізатор внутріпроцедурний (intraprocedural), а не межпроцедурний (interprocedural).

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

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

function g (arr) {return arr; } Function f (a, b, c) {var x = [a, b, c]; g (x); return x [0]; }

Оптимізатор може довести, що x [0] === a тільки якщо x не йде (escapes) з його області уваги - тобто тільки якщо він може відкрито підставити (inline) g в f і побачити, що g ніяк не впливає на вміст x.

Q: Як і чи потрібно прогрівати функції вручну?

На мій погляд прогрів функція вручну це досить складна і абсолютно не доцільна робота.

Всі функції поступово прогріваються самі в міру використання і оптимізуються на основі їх внутрішньої поведінки. V8 навіть здатний оптимізувати функції під час їх виконання, за допомогою техніки відомої як on stack replacement (OSR), підміняючи повільну неоптимізованими версію функції з довгим циклом всередині на оптимізовану версію посеред виконання цього самого циклу, причому оптимізація відбуватиметься в окремому потоці - не заважаючи самому циклу.

Я можу собі уявити випадок, коли ви використовуєте V8 на серевер і хочете, щоб піднімається сервер починав відповідати на запити з максимальною швидкістю з того самого моменту, коли ви перекладете на нього трафік. У цій ситуації можна зрозуміти бажання прогріти V8 пославши на сервер N запитів перед переведенням основого трафіку ... Я, однак, рекомендую робити це тільки в тому випадку, якщо ви розумієте на 100%, що ви робите і що відбувається всередині V8. Іншими словами, я не рекомендую цього робити :)

Q: Майбутнє JS оптимізацій

Це питання не технічний і не має простого технічного відповіді, але я вирішив його включити в дайджест з однієї причини: в наступних питаннях / відповідях буде відчувається одна і та ж тема - сучасні JS движки досить довгий час розвивалися вглиб приділяючи уваги конкретним паттернам програмування, які або були вже дуже поширені, або були включені в конкретні широко використовуються бенчмарки. Як результат багато речей залишилися за бортом прогресу, наприклад, arr.forEach (function (el) {}) може бути помітно повільніше, ніж зазвичай for-циклу, хоча в теорії абсолютно ясно як звести надлишкову вартість forEach до мінімуму. Аналогічно з сумною продуктивністю Function.prototype.bind в порівнянні з її рукописними аналогами. Приклади можна наводити нескінченно.

Я вважаю в розвитку JS двигунів зараз настав той момент, коли всі усвідомили, що занадто довго копали вглиб - і все поступово почнуть копати в ширину, розширюючи зону "швидко исполнимого JS".

Q: V8 vs. arguments object

Так, пишуть правду. Якщо всередині функції необережно поводитися з arguments, то Crankshaft відмовиться цю функцію оптимізувати, тому що він підтримує тільки три види використання arguments.

  • arguments [i] - взяття аргументу за індексом, причому вихід за межі масиву аргументів призводить до деоптімізаціі;
  • arguments.length
  • f.apply (x, arguments), де f.apply === Function.prototype.apply.

При цьому arguments можна зберігати в локальну змінну, але не можна в цій змінній змішувати з іншими значеннями. Ще V8 не любить, коли в non-strict функція змішують іменовані параметри і arguments.

function good () {var a = arguments; var b = new Array (a. length); for (var i = 0; i <a. length; i ++) b [i] = a [i]; return b; } Function bad1 () {var a = arguments; if (! a [0]) a = [1, 2, 3]; } Function bad2 () {return []. call. slice (arguments, 1); } Function bad3 (a) {return a? arguments [1]: 42; }

Q: Продуктивність apply, call, bind

  • Function.prototype.bind найповільніший з усіх - з історичних причин і плюс його ніхто не розганяє. Причому "повільний" відноситься як до самого bind, так і до тих функцій, які він виробляє. Доходить до абсурдної ситуації, що написаний на коленкте аналог bind, який реалізує тільки частина цієї семантики, може бути як в десятки разів швидше. Насправді цей факт, що можна замінити bind наколеночним поділитися і не чекати поки всі VM розженуть вбудований частково відповідальний за те, що вбудований ніхто і не розганяє. Дійсно - навіщо розганяти, якщо його ніхто не використовує?
  • Function.prorotype.apply на другому місці за швидкістю. Про нього важливо знати, що func.apply (o, arguments) - це один із спеціальних патернів, які розпізнаються Crankshaft (оптимізує комілятор V8), і цей патерн компілюється в дуже ефективний код. Якщо ж ви намагаєтеся передати arguments в якусь іншу функцію (наприклад, робите [] .slice.call (arguments, 1)), то Crankshaft взагалі відмовиться оптимізувати вашу функцію.
  • Function.prototype.call найшвидший з трьох. Головна перевага call в тому, що йому не потрібен тимчасовий масив аргументів. func.call (obj, x, y, z) очевидно виробляє менше сміття в порівнянні з func.apply (obj, [x, y, z]). Плюс відносно недавно Petka Antonov прісал патч, який навчив Crankshaft розпізнавати в func.call (obj, x, y, z) звичайний виклик і, наприклад, інлайн цільову функцію в цьому місці.

Crankshaft не вміє оптимізувати функції містять try {} catch (e) {} (і finally). Тому якщо функція гаряча, робить багато роботи і може бути прискорена оптимізаціями, які Crankshaft робить, то така функція дійсно стане швидше від розбиття її на дві - перша, яка робить роботу, і друга, яка її загортає в try / catch

function DoLotsOfComputation () {for (var i = 0; i <BigNumber; i ++) {// worky-worky}} function DoLostsOfComputationSafe () {try {DoLotsOfComputation (); } Catch (e) {// catchy-catchy}}

TurboFan - оптимізатор, який зараз знаходиться в розробці і в майбутньому замінить Crankshaft, вміє оптимізувати функції містять try / catch, тому через деякий час ця рада стане неактуальним.

Q: V8 vs. forEach

Array.prototype.forEach в V8 насправді написаний на звичайному JS (Self hosted). Проблема в тому, що ніхто поки не навчив оптимізатор як видаляти будь-які надлишкові перевірки, яким цей forEach нашпигований. З історичних причин Crankshaft'у навіть заборонено інлайн forEach в викликає його код, а це б дуже важливий перший крок, який би дозволив спеціалізувати код forEach під той масив, на якому його викликають.

Все вищесказане стосується до всіх "функціональним" методам на Array.prototype (map, reduce, etc).

Q: defineProperty vs V8

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

var Constants = {} Object. defineProperty (Constants, "AnswerToTheUltimateQuestion", {value: 42, // By default: // writable: false, // configurable: false}) function foo () {return Constants. AnswerToTheUltimateQuestion; }

Якщо foo буде Оптимізована, то V8 просто підставить 42 замість Constants.AnswerToTheUltimateQuestion.

Пастка hidden class transition collision

defineProperty дозволяє створювати так називаеммие accessor properties і з ними пов'язана одна пастка - accessor найкраще садити на прототип, а не створювати новий на кожному новому об'єкті (втім, accessor можна без остраху садити на об'єкт Сінглтон).

Демка цієї проблеми ( відкрити в IRHydra ):

Загадки про продуктивність

Загадка №1: Map vs. object

Суть цієї загадки полягає в наступному: ми генеруємо масив випадкових рядків, заповнюємо об'єкт і ES6 Map цими рядками як ключами, а потім починаємо міряти що швидше obj [keys [i]] або map.get (keys [i]).

Раптово виявляється, що obj [keys [i]] помітно швидше, але тільки якщо ми використовуємо keys = Object.keys (obj), замість оригінальних keys. Спрощена версія загадки виглядає так:

function randomString () {var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; var textLength = Math. floor (Math. random () * possible. length); for (var i = 0; i <textLength; i ++) text + = possible. charAt (Math. floor (Math. random () * possible. length)); return text; } Var keys = []; for (var i = 0; i <1000; i ++) {keys. push (randomString ()); } Var obj = Object. create (null); var map = new Map (); for (var key of keys) {obj [key] = key; map. set (key, key); } // 10k ops / sec for (var i = 0; i <keys. Length; i ++) obj [keys [i]]; // 19k ops / sec for (var i = 0; i <keys. Length; i ++) map. get (keys [i]); var objectKeys = Object. keys (obj); // 10k ops / sec for (var i = 0; i <keys. Length; i ++) map. get (objectKeys [i]); // 100k ops / sec for (var i = 0; i <keys. Length; i ++) obj [objectKeys [i]];

Здавалося б вміст objectKeys точно таке ж як keys, як obj [keys [i]] може бути швидше obj [objectKeys [i]]?

Відгадка полягає в тому, що швидкий шлях (fast path) операції obj [k] підтримує тільки Інтерналізована рядки , А V8 інтерналізуются тільки деякі рядки (наприклад, рядкові літерали або імена властивостей), і не інтерналізуются результат конкатенації. Іншими словами рядка в масиві keys НЕ Інтерналізована, а рядки з масиві objectKeys, хоч і рівні рядках з keys за змістом - Інтерналізована, тому що це імена властивостей obj.

Визначальне властивість інтерналізованих рядків полягає в тому, що дві Інтерналізована рядки можуть бути порівняні простим порівнянням покажчиків. Порівняння інших строкових уявлень (при збігу довжин і хеш) вимагає порівняння їх вмісту. Саме тому швидкий шлях операції obj [k] для об'єктів зі словниковим поданням сховища властивостей і підтримує виключно Інтерналізована ключі k, всі інші ключі обробляються загальним кодом всередині середовища виконання, який набагато повільніше цього fast path.

Шпаргалка: уявлення рядків в V8.

Будь-яка рядок в V8 складається з трьох фіксованих полів тип, хеш, довжина і вмісту, подання якого залежить від конкретного типу рядка.

+ ------- + | | type descriptor (aka map) + ------- + | | hash + ------- + | | length + ------- + | + - + | | | ~~~~~~~~~> payload | | | | + - + + ------- +

Плоскі рядки (sequential, flat) просто містять в собі всі свої символи:

+ ------- + | | + ------- + | | + ------- + | | + ------- + | xxxx + - + | xxxx | | ~~~~~~~~~> characters | xxxx | | | xxxx + - + + ------- +

Cons-рядки використовуються для представлення результатів конкатенації без реального копіювання вмісту конкатеніруемих рядків. Наприклад, A + B буде представлена ​​як

+ ------- + | | + ------- + | | + ------- + | | + ------- + | * --- + ---> string A + ------- + | * --- + ---> string B + ------- +

Фактично це аналог структури даних rope .

Sliced-рядки використовуються для представлення результатів операції взяття подстроки без реального копіювання символів. Наприклад, A.substring (1) може бути представлена ​​так:

+ ------- + | | + ------- + | | + ------- + | | + ------- + | * --- + ---> string A + ------- + | 1 | offset within A + ------- +

Ще є так звані зовнішні рядки, існуючі для економії пам'яті при встановленні V8 в інші проекти (наприклад, щоб не зберігати одну і ту ж рядок два рази - і в всередині DOM реалізації на C ++ і всередині V8):

+ ------- + | | + ------- + | | + ------- + | | + ------- + | * --- + ---> v8 :: String :: ExternalStringResource (C ++) + ------- +

V8 відстежує які символи використовуються всередині рядка і вибирає однобайтовое (Latin1) або двох-байтовое (UTF16) уявлення рядки автоматично для економії пам'яті.

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

Загадка №2: на Stack Oveflow

Баг в Uint32 оптимізації в V8

Q: Як можна подивитися оптимізований код в Рантайм?
Поки що?
Q: Як і чи потрібно прогрівати функції вручну?
Slice (arguments, 1); } Function bad3 (a) {return a?
Дійсно - навіщо розганяти, якщо його ніхто не використовує?

Новости

Отель «Централь» Официальный сайт 83001, Украина, г. Донецк, ул. Артема, 87
Тел.: +38 062 332-33-32, 332-27-71
[email protected]
TravelLine: Аналитика


Студия web-дизайна Stoff.in © 2008