Що браузери роблять з вашим javascript-кодом про оптимізацію в js-двигунах на прикладі v8
Оптимізація коду починається не стільки з вивчення особливостей мови програмування, скільки з розуміння схеми роботи всієї «технологічного ланцюжка», яка задіяна при створенні програми - від алгоритму програми до компілятора.
В даний час В'ячеслав активно працює в Google над Dart VM.
У цьому інтерв'ю він розповів про те, що відбувається всередині движка, що виконує динамічний JS-код і поділився прикладами, як виконуються деякі оптимізації і чому важливо глибоко розуміти роботу движка, щоб забезпечити швидке виконання коду.
- Закінчив мехмат МДУ. Завжди цікавився компиляторами. Спочатку працював в Одессаой компанії Excelsior, де люди роблять свою власну JVM з AOT-компілятором, потім пішов в Google. В Google спочатку займався V8, потім Dart VM, якийсь час навіть лагодив різні баги а LuaJIT.
- Багато хто віддає перевагу машину V8 за її здатність до оптимізації коду. В чому секрет?
- Основна складність при оптимізації динамічних мов програмування полягає в тому, що для досягнення пікової продуктивності віртуальна машина повинна, по суті справи, вгадати намір програміста і побачити статичну структуру, приховану в динамічному коді. Сильною рисою V8 є те, що вона вміє досить добре цю саму структуру помічати.
і виявляється, що V8 цілком здатний скомпілювати тіло цієї функції в досить компактний машинний код:
Це зовсім нетривіальне завдання в умовах, коли всі динамічно типізованих і статично абсолютно незрозуміло, що таке
- А що стосується слабких сторін движка?
- Іноді сильна боку V8 стає її слабкою стороною. Відбувається це з двох причин.
По-перше, не в будь-якому динамічно типизированном коді V8 здатна побачити статичну структуру, яка, можливо, і була очевидна написав цей код програмісту. Десь це відбувається, тому що в V8 щось ще не реалізовано, десь - бо алгоритмічно не завжди можливо.
Один з найпоширеніших прикладів - це код в стилі:
Тут V8 не вміє помічати, що a.f і b.f мають одне і теж поведінку (з поправкою на значення захоплених змінних), тому що це функції, створені з одного і того ж функціонального литерала (function literal).
По-друге, іноді оптимізуючий компілятор V8 просто не підтримує якусь конструкцію в коді, і тому компілятор відмовляється дивитися на код. Наприклад, Crankshaft (це перша реалізація ідеї адаптивної компіляції в V8) ніколи не підтримував try-catch / finally і тому відмовлявся оптимізувати функції, де він був присутній. Зараз функції, які використовують try-catch, компілюються через TurboFan (компілятор, що розробляється на заміну Crankshaft), тому ситуація виправляється.
По-третє, іноді витрати, що витрачаються на виявлення статичної структури, не окупаються. V8 витрачає час, будує дерева прихованих класів, оптимізує / деоптімізірует / оптимізує знову код - а код краще не стає, наприклад, тому що об'єкти здебільшого використовуються як словники. Це дуже цікава проблемна область - як правильно балансувати витрати на оптимізацію коду і поліпшення продуктивності від цієї оптимізації. Як виконувати код з розумною швидкістю, навіть якщо цей код не потрапляє на очевидний fast path.
Роботи над усіма цими проблемами ведуться, а тому в будь-яких випадках, коли ваш код працює повільно на V8, слід скаржитися розробникам V8 - хоча б для того, щоб вони пояснили, чому код працює повільно або може бути навіть полагодили щось в самій V8 .
- Як ви вважаєте, чи має сенс оптимізація коду під певний движок (в нашому випадку - V8)? Адже буває так, що в якомусь браузері код гальмує. Що тоді робити?
- Тут можливі два варіанти (або їх комбінація):
- ваш код «поганий». В цьому випадку зазвичай страждає продуктивність відразу під кількома двигунами, і оптимізація під один движок покращує продуктивність коду відразу під кількома;
- код движка «поганий». У цьому випадку, як я зазначив раніше, треба відсилати баг репорт розробникам движка, які можуть або полагодити сам движок, або часто рекомендувати спосіб обійти цей баг.
- Чи можете ви перелічити найбільш часто зустрічаються «граблі», на які наступають розробники в своєму коді (тобто по суті, найбільш часто зустрічаються помилки в коді, сильно впливають на продуктивність при орієнтації на V8 - в рамках згаданої вище ситуації «код поганий »)?
- Буває два типи грабель. Перший тип - граблі алгоритмічні. Наприклад, іноді люди ітеріруют по рядку, використовуючи цикл в стилі:
Багато компіляторщікі починають злегка підстрибувати на стільці і потирати руки, коли бачать подібний код, оскільки реалізувати оптимізацію, яка б такий цикл перетворювала б у щось осудна - це моторошно цікава задача. Однак з точки зору розробника набагато ефективніше розуміти, скільки коштує s.substring в кращому і гіршому випадку, і такого коду не писати. Тому що без хитрих оптимізацій всередині движка цей цикл має тимчасову складність O (n 2).
Інший тип грабель - це граблі, пов'язані з оптимізацією динамічних мов (я вже це торкнувся вище). Наприклад, поліморфний код, тобто код, який працює з різними типами об'єктів. Такий код для V8 часто як криптоніт (кристалічна радіоактивну речовину, яке фігурує у всесвіті DC Comics. Криптоніт знаменитий завдяки тому, що є єдиною немагіческой слабкістю Супермена та інших кріптонцев - він здатний надавати на них вплив, яке різниться в залежності від кольору мінералу. - прим . ред.) для Супермена. Тема це досить глибока, і у мене на цю тему є цілий пост з картинками.
- Як же боротися з цими проблемами? Шукати «правила написання коду під певний движок» і перевіряти свій код на відповідність їм?
- А якщо повернутися до згаданих вами проблемами коду компілятора (ситуації «код движка поганий»)? Чи можна навести якісь найбільш часто відзначаються проблеми 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? Чи потрібно вже зараз розробнику переходити на нову мову? Яке його становище в індустрії?
З цікавих речей, які зараз відбуваються з мовою за межами Web, можна виділити flutter.io - це фреймворк для розробки кроссплатформенних (Android iOS) мобільних додатків і dartino - маленький Dart для вбудованих систем.
Дякуємо за розмову!
Приклади, наведені вище, наочно ілюструють, що одного лише переліку «простих рекомендацій під конкретний движок» для оптимізації коду мало - важливо розуміти принцип його роботи, а заодно і чітко уявляти, яких результатів хочеться домогтися від програми.