система онлайн-бронирования
г. Донецк, Украина, ул. Артёма, 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