Що браузери роблять з вашим JavaScript-кодом: про оптимізацію в JS-движках на прикладі V8

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

Ми поговорили з В'ячеславом Єгоровим, інженером з Google, компиляторщиком до мозку кісток, який працював над JavaScript движком під назвою V8, вбудованим в Chromium (і, як наслідок Chrome, Android версію браузера, хмарну операційну систему Chrome OS) і менш відомий Maxthone.
JavaScript-програмістам В'ячеслав, швидше за все, відомий як автор постів про нутрощі V8 і як доповідач, захоплено показує машинний код на конференціях для Web-розробників.

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


Движок V8 був розроблений датським підрозділом компанії Google і розповсюджується за ліцензією BSD. Движок написаний на C++ і підтримує специфікацію ECMA-262.
Перша версія движка з'явилася в 2008 році. На поточний момент активна розробка движка триває здебільшого в мюнхенському офісі Google.
Серед основних особливостей движка — компіляція javascript безпосередньо в машинний код, адаптивна оптимізація і деоптимизация коду під час компіляції, швидкий збір сміття.
— Розкажіть, будь ласка, в двох словах про себе і про роботу в Google, пов'язаної з JavaScript-движок V8?

— Закінчив мехмат МДУ. Завжди цікавився компіляторами. Спочатку працював в Новосибірської компанії Excelsior, де люди роблять свою власну JVM з AOT-компілятором, потім пішов в Google. У Google спочатку займався V8, потім Dart VM, якийсь час навіть лагодив різні баги а LuaJIT.

— Багато хто воліє машину V8 через її здатність до оптимізації коду. У чому секрет?

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

Приміром, хтось написав на JavaScript:

function len(p) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}

і виявляється, що V8 цілком здатний скомпілювати тіло цієї функції у досить компактний машинний код:

vmovsd xmm1,[rax+0x17]
vmulsd xmm1,xmm1,xmm1
vmovsd xmm2,[rax+0x1f]
vmulsd xmm2,xmm2,xmm2
vaddsd xmm1,xmm2,xmm1
vsqrtsd xmm1,xmm1,xmm1

Це зовсім нетривіальне завдання в умовах, коли всі динамічно типизировано і статично абсолютно незрозуміло, що таке
p.x

або навіть
Math.sqrt
.
— А що стосується слабких сторін движка?

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

Один з найпоширеніших прикладів — це код в стилі:

function make(x) {
return { f: function () { return x } }
}
var a = make(0), b = make(0);

Тут V8 не вміє помічати, що a.f b.f мають одне і теж поведінку (з поправкою на значення захоплених змінних), т. к. це функції, створені з одного й того ж функціонального буквальне (function literal).

По-друге, іноді оптимізуючий компілятор V8 просто не підтримує якусь конструкцію в коді, і тому компілятор відмовляється дивитися на код. Наприклад, Crankshaft (це перша реалізація ідеї адаптивної компіляції V8) ніколи не підтримував try-catch/finally і тому відмовлявся оптимізувати функції, де він був присутній. Зараз функції, які використовують try-catch, компілюються через TurboFan (компілятор, що розробляється на заміну Crankshaft), тому ситуація виправляється.

По-третє, іноді витрати, що витрачаються на виявлення статичної структури, не окупаються. V8 витрачає час, будує дерева прихованих класів, оптимізує/ деоптимизирует/ оптимізує знову код — а код краще не стає, наприклад, тому що об'єкти здебільшого використовуються словники. Це дуже цікава проблемна область — як правильно збалансувати витрати на оптимізацію коду і поліпшення продуктивності від цієї оптимізації. Як виконувати код з розумною швидкістю, навіть якщо цей код не потрапляє на очевидний fast path.

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

До речі, все сказане вище в тій чи іншій мірі відноситься до будь JavaScript движку, який хоч якось намагається оптимізувати ваш код. Всі движки намагаються вгадати, що ж програміст хотів сказати своєю програмою, і намагаються побачити статичну в динамічному. У всіх движків є поділ на fast path і slow path і свої власні особливості.

— Як ви вважаєте, чи має сенс оптимізація коду під певний движок (в нашому випадку — V8)? Адже буває так, що в якомусь браузері код гальмує. Що тоді робити?

— Тут можливо два варіанти (або їх комбінація):

  • ваш код «поганий». В цьому випадку зазвичай страждає продуктивність відразу під кількома двигунами, і оптимізація під один движок покращує продуктивність коду відразу під кількома;

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

— Буває два типи грабель. Перший тип — граблі алгоритмічні. Наприклад, іноді люди итерируют по рядку, використовуючи цикл в стилі:

for (; s != ""; s = s.substring(1))

Багато компиляторщики починають злегка підстрибувати на стільці і потирати руки, коли бачать подібний код, оскільки реалізувати оптимізацію, яка б такий цикл перетворювала б у щось осудна — це дуже цікаве завдання. Однак з точки зору розробника набагато ефективніше розуміти, скільки коштує s.substring у найкращому та найгіршому випадку, і такого коду не писати. Тому що без хитрих оптимізацій всередині движка цей цикл має тимчасову складність O(n2).

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

— Як же боротися з цими проблемами? Шукати «правила написання коду під певний движок» і перевіряти свій код на відповідність їм?

— Тут найголовніше — усвідомити, що проблеми з продуктивністю не можна вирішити наскоком. Припустимо, ви почули про поліморфізм і побігли, втрачаючи тапки, переписувати весь свій код в мономорфном стилі. Нічого хорошого з цього зазвичай не виходить. Тут потрібен зовсім інший підхід, який дуже добре описується класичною російською приказкою «сім разів відміряй, один раз відріж». Потрібно вибудувати у своїй голові ментальну модель того, як працює VM і того, як працює ваш код, потрібно гарненько попрофилировать, зрозуміти куди у вас витікає продуктивність, і вже потім задаватися питаннями оптимізації. Часто виявляється, що V8 робить свою роботу добре, а справжнє пляшкове горлечко зовсім навіть не у javascript коді.

— А якщо повернутися до згаданих вами проблемами коду компілятора (ситуації «код системи поганий»)? Можна навести якісь найбільш часто спостерігаються проблеми V8?

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

Як приклад з практики: мене одного разу попросили подивитися на код, який чомусь іноді починав працювати дуже повільно. Виявилося все через вираження Math.floor(x * y), в якому іноді x ставав рівним -1, а y рівним 0. Здавалося б нічого особливого, але Math.floor(x * y) в такому випадку дорівнює чарівного «негативний нуль» (який майже зовсім як 0, але якщо поділити, скажімо, 1 на нуль, то виходить позитивна нескінченність, а якщо поділити 1 на мінус нуль, то виходить негативна нескінченність). Crankshaft-завжди припускав, що результат операції floor — це число ціле, і тому -0 — число не можна представити у вигляді цілого, викликало деоптимизацию. Рішенням проблеми в даному конкретному випадку було замінити Math.floor(x * y) (x * y) | 0 (взагалі, це не еквівалентне перетворення, але для коду, який потрібно було розігнати, не грало ролі). До речі, нещодавно цю проблему в Crankshaft полагодили раз і назавжди.

Я спеціально вибрав цей баг в якості прикладу, т. к. він досить загадковий («що за мінус-нуль?», «що за деоптимизация?») у надії переконати читача в тому, що знання про конкретних баги абсолютно марно. Я виявив, що код, на який я дивився, наступає на цей баг не тому, що я знав, що Math.floor не терпить-0, і озброєний цим знанням пішов замінювати всі Math.floor |0, поки баг сам не виправився… Ні, я знайшов цей баг, оскільки знав, як профілювати V8, з якими ключами треба її запускати, щоб V8 мені показала список деоптимизаций. Тому найважливіше розуміти, як V8 (та інші JS VM) працюють.
Володіючи цим знанням, завжди можна з'ясувати, «що ж пішло не так десь у глибоких підземеллях», і потім скаржитися у вищі інстанції.
Я про це досить багато пишу у своєму блог і навіть зробив тулзу, яка дозволяє дивитися інформацію, що видається V8 (compiler IR, deopts, etc), у більш-менш зручній формі.

— Ви зараз займаєтеся Dart? Коли ж цей новий мову замінить JS? Треба вже зараз розробнику переходити на нову мову? Яке його становище в індустрії?

— Взяти і ось просто так замінити JS у всій нашій всесвіту не можна. А ось на окремих робочих місцях — цілком можна. Люди замінюють його різними речами, хтось ClojureScript, хтось TypeScript, а хтось Dart. Мови програмування — це штука складна і конфліктна. Всі мають на їх рахунок свою думку. Тому моя порада зазвичай простий — втомилися від JavaScript? Можна сходити на dartlang.org завантажити SDK (або погратися з мовою прямо в браузері) і вирішити для самого себе.

З цікавих речей, які зараз відбуваються з мовою за межами Web, можна виділити flutter.io — це фреймворк для розробки кросплатформених (Android & iOS) мобільних додатків і dartino — маленький Dart для вбудованих систем.

Дякуємо за розмову!

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

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

0 коментарів

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