Тут розробник V8. Враховуючи велику зацікавленість цим питанням та відсутність інших відповідей, я можу дати це зняти; Боюся, це не буде відповіддю, на який ви сподівалися.
Чи є десь настанови, як програмувати, залишаючись у світі запакованих масивів SMI (наприклад)?
Коротка відповідь: саме тут : const guidelines = ["keep your integers small enough"]
.
Більш довга відповідь: давати вичерпний набір вказівок важко з різних причин. Загалом, наша думка полягає в тому, що розробники JavaScript повинні писати код, який має сенс для них та їх використання, а розробники двигунів JavaScript повинні з'ясувати, як швидко запустити цей код на своїх двигунах. З іншого боку, очевидно, що в цьому ідеалі є деякі обмеження, в тому сенсі, що деякі схеми кодування завжди матимуть більш високі витрати на продуктивність, ніж інші, незалежно від вибору впровадження двигуна та зусиль з оптимізації.
Коли ми говоримо про поради щодо продуктивності, ми намагаємось пам’ятати про це та ретельно оцінюємо, які рекомендації мають велику ймовірність залишатися дійсними протягом багатьох двигунів і протягом багатьох років, а також є досить ідіоматичними / ненав'язливими.
Повернення до прикладу: внутрішнє використання Smis має бути детальною інформацією про реалізацію, про яку не потрібно знати користувальницькому коду. Це зробить деякі випадки більш ефективними, а в інших випадках не зашкодить. Не всі двигуни використовують Smis (наприклад, AFAIK Firefox / Spidermonkey в минулому історично не було; я чув, що в деяких випадках вони використовують Smis в наші дні; але я не знаю жодних деталей і не можу спілкуватися з будь-яким органом на причина). У V8 розмір Smis є внутрішньою деталлю і фактично змінюється з часом і над версіями. На 32-бітних платформах, які раніше були випадками більшості, Smis завжди були 31-бітовими цілими підписами; на 64-бітних платформах вони раніше були 32-бітовими цілими числами, що останнім часом здавалося найпоширенішим випадком, поки в Chrome 80 ми не постачали "стискання вказівника" для 64-розрядних архітектур, для яких потрібно зменшити розмір Smi до 31 біта, відомого з 32-розрядних платформ. Якщо у вас трапилася реалізація на припущенні, що Smis, як правило, становить 32 біти, у вас виникнуть нещасні ситуації, наприкладце .
На щастя, як ви зазначили, подвійні масиви все ще дуже швидкі. Для числово-важкого коду, мабуть, має сенс припустити / націлити подвійні масиви. Враховуючи поширеність пар у JavaScript, доцільно припустити, що всі двигуни мають хорошу підтримку парних та подвійних масивів.
Чи можливо робити універсальне високоефективне програмування в Javascript, не використовуючи щось на зразок макросистеми для вбудовування таких речей, як vec.add () в callites?
"загальний", як правило, суперечить "високопродуктивним". Це не пов'язано з JavaScript або конкретними реалізаціями двигуна.
"Загальний" код означає, що рішення повинні прийматися під час виконання. Кожен раз, коли ви виконуєте функцію, код повинен запускатися, щоб визначити, скажімо, "це x
ціле число? Якщо так, перейдіть до цього кодового шляху. Чи x
є рядок? Потім перестрибніть сюди. Це об'єкт? Чи є .valueOf
? Ні? Потім?" можливо .toString()
? Можливо, в її прототипі ланцюжок? Зателефонуйте це і перезавантажте спочатку з його результатом ". "Високопродуктивний" оптимізований код по суті побудований на ідеї відмовитись від усіх цих динамічних перевірок; це можливо лише тоді, коли двигун / компілятор має якийсь спосіб вивести типи достроково: якщо він може довести (або припустити з достатньо високою ймовірністю), що x
завжди буде цілим числом, тоді йому потрібно лише генерувати код для цього випадку ( охороняється за допомогою перевірки типу, якщо були задіяні недоведені припущення).
Вкладиш є ортогональним для всього цього. "Загальну" функцію все ще можна накреслити. У деяких випадках компілятор, можливо, зможе поширити інформацію про тип у вкладеній функції, щоб зменшити там поліморфізм.
(Для порівняння: C ++, будучи статично складеною мовою, має шаблони для вирішення пов'язаної проблеми. Коротше кажучи, вони дозволяють програмісту чітко доручити компілятору створити спеціалізовані копії функцій (або цілі класи), параметризовані для заданих типів. Це приємне рішення для деяких випадків, але не без власного набору недоліків, наприклад, тривалий час компіляції та великі двійкові файли. У JavaScript, звичайно, немає такого поняття, як шаблони. Ви можете використовувати eval
для створення системи, яка дещо схожа, але тоді ви я зіткнувся з подібними недоліками: вам доведеться виконати еквівалент роботи компілятора C ++ під час виконання, і вам доведеться турбуватися про велику кількість коду, який ви генеруєте.)
Як один модульний код високої продуктивності перетворюється на бібліотеки у світлі таких речей, як мегаморфні сайти викликів та деоптимізація? Наприклад, якщо я щасливо використовую пакет лінійної алгебри A з високою швидкістю, а потім імпортую пакет B, який залежить від A, але B називає його іншими типами і деоптимізує його, раптом (без зміни коду) мій код працює повільніше .
Так, це загальна проблема з JavaScript. V8 використовувався для внутрішньої реалізації певних вбудованих елементів (таких як Array.sort
) у JavaScript, і ця проблема (яку ми називаємо "забрудненням зворотним зв'язком") була однією з головних причин, чому ми повністю віддалилися від цієї техніки.
Це означає, що для числового коду існує не так багато типів (лише Smis і подвійні), і, як ви зазначили, вони повинні мати схожі результати на практиці, тому, хоча забруднення зворотного зв’язку типу дійсно є теоретичним питанням, а в деяких випадках може мають суттєвий вплив, також досить ймовірно, що в сценаріях лінійної алгебри ви не побачите вимірної різниці.
Крім того, всередині двигуна є набагато більше ситуацій, ніж "один тип == швидкий" і "більше одного типу == повільний". Якщо дана операція бачила як Smis, так і подвійну, це абсолютно добре. Завантаження елементів із двох видів масивів теж добре. Ми використовуємо термін "мегаморфний" для ситуації, коли навантаження побачило стільки різних типів, що відмовилися відстежувати їх окремо, а замість цього використовується більш загальний механізм, який краще масштабує велику кількість типів - функція, що містить такі навантаження, може як і раніше оптимізувати. "Деоптимізація" - це дуже специфічний акт, що потрібно викинути оптимізований код для функції, оскільки видно новий тип, який раніше не бачив, і тому оптимізований код не підходить для обробки. Але навіть це добре: просто поверніться до неоптимізованого коду, щоб зібрати більше відгуків про тип, та пізніше оптимізуйте його. Якщо це трапляється пару разів, то турбуватися нема про що; це стає проблемою лише у патологічно поганих випадках.
Отже, підсумок всього, що є: не хвилюйтеся з цього приводу . Просто напишіть розумний код, нехай двигун цим займається. І під поняттям "розумне" я маю на увазі: те, що має сенс для вашого випадку використання, є читабельним, ремонтопридатним, використовує ефективні алгоритми, не містить помилок, як читання за межами довжини масивів. В ідеалі це все, що вам належить, і вам більше нічого не потрібно робити. Якщо ви відчуваєте себе краще щось робити та / або якщо ви насправді спостерігаєте за ефективністю, я можу запропонувати дві ідеї:
Використання TypeScript може допомогти. Велике попередження про жирність: типи TypeScript спрямовані на продуктивність розробника, а не на продуктивність виконання (і як виявляється, ці дві перспективи мають дуже різні вимоги до типової системи). Однак, є певне перекриття: наприклад, якщо ви послідовно коментуєте речі number
, компілятор TS попередить вас, якщо ви випадково помістили null
масив або функцію, яка повинна містити / працювати лише на числах. Звичайно, все ж потрібна дисципліна: один number_func(random_object as number)
люк для втечі може мовчки підірвати все, тому що правильність анотацій типу ніде не застосовується.
Використання TypedArrays також може допомогти. Вони мають трохи більше накладних витрат (споживання пам’яті та швидкість розподілу) на масив порівняно зі звичайними масивами JavaScript (тому, якщо вам потрібно багато малих масивів, то звичайні масиви, ймовірно, більш ефективні), і вони менш гнучкі, оскільки не можуть рости або зменшуються після розподілу, але вони гарантують, що всі елементи мають саме один тип.
Чи є якісь хороші прості у використанні інструменти вимірювання для перевірки того, що працює двигун Javascript внутрішньо з типами?
Ні, і це навмисно. Як було пояснено вище, ми не хочемо, щоб ви спеціально адаптували свій код до будь-яких моделей, які V8 сьогодні можуть особливо оптимізувати, і ми не віримо, що ви теж цього хочете зробити. Цей набір речей може змінюватися в будь-якому напрямку: якщо є шаблон, який ви хотіли б використовувати, ми можемо оптимізувати це в майбутній версії (раніше ми грали з ідеєю зберігання 32-бітових цілих безбіткових цілих чисел як елементів масиву .. але робота над цим ще не почалася, тому жодних обіцянок немає); а іноді, якщо є модель, яку ми використовували для оптимізації в минулому, ми можемо вирішити відмовитись від цього, якщо це перешкоджає іншим, більш важливим / вражаючим оптимізаціям. Крім того, такі речі, як вкраплення евристики, як відомо, важко виправити, тож прийняття правильного ухваленого рішення в потрібний час є сферою постійних досліджень та відповідних змін поведінки двигуна / компілятора; що робить це ще одним випадком, коли було б прикро для всіх (тита нас) якщо ви витратили багато часу на виправлення свого коду, поки якийсь набір поточних версій браузера не зробить приблизно найважливішими рішення, які ви вважаєте (чи знаєте?) найкращими, лише повернувшись через півроку, щоб зрозуміти, що зараз діючі браузери змінили свою евристику.
Ви, звичайно, завжди можете виміряти ефективність вашої програми в цілому - ось що в кінцевому рахунку має значення, а не те, який вибір конкретно двигун зробив внутрішньо. Остерігайтеся мікро-показників, оскільки вони вводять в оману: якщо ви витягнете лише два рядки коду та порівняльні показники, то ймовірність того, що сценарій буде досить різним (наприклад, зворотний зв'язок різного типу), що двигун буде приймати дуже різні рішення.