Векторні обчислення JS, чи є сенс, коли і як можна використовувати SIMD в браузері

Все більше і більше область застосування мови програмування javascript відходить від руху кнопочками в браузері так перефарбовування фону в бік складних і об'ємних веб-додатків. Вже у всю світом крокує технологія WebGL, що дозволяє відображати тривимірні сцени в браузері прямо на мові js, а разом з нею і ускладнюються завдання.
Продуктивність машин продовжує зростати, а разом з нею і мова обзаводиться новими виражальними засобами, що дозволяють прискорювати обчислення. І поки WebAssembly десь там у далекому і світлому майбутньому, asm.js застряг в болоті і звернув з шляху, найближчим часом спочатку як частина es2015, нині як окремий стандарт виходить підтримка векторних операцій в JS.
Всі, кому цікаво, що таке SIMD і векторні обчислення, як ними користуватися в js, а так само що дає їх використання — прошу під кат.

Відступ від автора: у вас не дежавю, ця стаття дійсно є перезаливом попередньої. На жаль, тоді я не доклав достатньо зусиль, щоб отримати якісну статтю, а тести не придатні ні для чого, як наслідок — я отримав в корені неправильні висновки, на що справедливо отримав критику. "Хабр торт і всяке потребье тут не прокотить" зрадів я, прикинув, що моя стаття поки одна з небагатьох, які спливають на запити "SIMD JS" і було б неправильно поширювати неправдиву інформацію, та ще й низької якості. У даній статті намагаюся виправити, ту нещадно приховав і видалив як страшний сон і повчальний урок про співвідношення часу, витраченого на статтю, до її якості.
Теорія
Припустимо, що вам необхідно зробити якусь просту операцію з кожним елементом масиву. Візьмемо суму з константою C, напишемо приклад на мові C
int arr[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
const c = 5;
for (int i = 0; i < 16; i++) {
arr[i] = arr[i] + c;
}

Меткі люди можуть спробувати трохи оптимізувати цей приклад, написавши щось на кшталт:
int arr[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
const c = 5;
for (int i = 0; i < 16; i += 4) {
arr[i] = arr[i] + c;
arr[i + 1] = arr[i + 1] + c;
arr[i + 2] = arr[i + 2] + c
arr[i + 3] = arr[i + 3] + c;
}

Оптимізація сумнівна, але, насправді, спрацює. Причиною цього є не те, що ми набагато рідше робимо операцію i = i + 1; (адже вона все одно відбувається при взятті індексу), а те, що процесор в цьому випадку може завантажувати і виконувати операції з великим захлестыванием один на одного в конвеєрі, доки операції не залежать один від одного, що дозволяє йому виробляти внутрішні операції, аж до виконання цих операцій одночасно і пакетної завантаження наступної частини масиву.
Проблема в тому, що він може здійснити ці оптимізації, а може і не зробити. Ми на це в явному вигляді вплинути не можемо, а накладки все одно зберігаються. І якщо сума обчислюється швидко, то який-небудь квадратний корінь вже виконується досить помітне час щоб з'явилося бажання змусити процесор виконувати цю операцію одночасно для кількох чисел.
Приблизно так подумали багато років тому (у 70-х роках) десь у надрах Texas Instruments і CDC, зробивши перші векторні супер-комп'ютери. Однак до них якийсь Майкл Флінн запропонував свою таксономію (класифікацію) комп'ютерів, однією з яких були SIMD. На жаль, на цьому нитка історії втрачається, але ми тут не для цього зібралися.
Таким чином, у 70-х роках минулого сторіччя з'явилися перші процесори, що дозволяли за раз вважати кілька однотипних операцій над числами. Пізніше це перетягнули до себе практично все у вигляді розширеного набору інструкцій.
Графічно класична архітектура виглядає наступним чином:
image
Нам потрібно скласти 4 пари чисел, тому ми змушені 4 рази викликати інструкцію add в процесорі.
Векторна операція виглядає наступним чином:
image
Коли нам потрібно скласти 4 пари чисел, ми просто викликаємо одну інструкцію, яка складає їх за один такт.
Невелика ремарка з приводу "векторна операція" і SIMD-операція. Справа в тому, що SIMD — більш загальне поняття, що припускає під собою виконання в один і той же момент часу одного або декількох однакових операцій над різними даними. У CUDA в кожен момент часу нитки виконують одну і ту ж операцію над різними даними, але цих операцій виконується стільки, скільки доступно потоків у відеокарті. Векторна арифметика має на увазі те, що виконується саме одна операція, причому, фактично, вона виконується просто над двома додатковими даними, складовими з себе впорядковано лежать кілька чисел в одній клітинці. Таким чином, векторні операції входять як підмножина в SIMD-операції, проте в ES2017 йдеться саме про векторної арифметиці, не знаю, чому вони так вирішили узагальнити, далі ми будемо вважати ці два поняття одним і тим же в рамках цієї статті.
Так що, виходить, ми можемо збільшити продуктивність своїх js додатків в 4 рази? (Протяжно)Нууу не зовсім. Але спершу поглянемо, як це робиться на C (gcc)
C і асемблер тут. Так, у статті про jsСкалярний приклад
void main() {
int a[4] = { 1, 3, 5, 7 };
int b = 3;
int c[4];

c[0] = a[0] + b;
c[1] = a[1] + b;
c[2] = a[2] + b;
c[3] = a[3] + b;
}

Його векторний аналог:
#include <xmmintrin.h>

extern void printv(__m128 m);

int main()
{
__m128 m = _mm_set_ps(-4, -3, -2, -1);
__m128 one = _mm_set1_ps(1.0 f);

printv(_mm_add_ps(m, one)); // Multiply by one (nop)

return 0;
}

(Приклади взяті з ресурсу liranuna.com)
Перший варіант стане в asm щось типу:
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $64, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movl $1, -48(%rbp)
movl $3, -44(%rbp)
movl $5, -40(%rbp)
movl $7, -36(%rbp)
movl $3, -52(%rbp)
movl -48(%rbp), %edx
movl -52(%rbp), %eax
addl %edx, %eax
movl %eax, -32(%rbp)
movl -44(%rbp), %edx
movl -52(%rbp), %eax
addl %edx, %eax
movl %eax, -28(%rbp)
movl -40(%rbp), %edx
movl -52(%rbp), %eax
addl %edx, %eax
movl %eax, -24(%rbp)
movl -36(%rbp), %edx
movl -52(%rbp), %eax
addl %edx, %eax
movl %eax, -20(%rbp)
nop
movq -8(%rbp), %rax
xorq %fs:40, %rax
je .L2
call __stack_chk_fail

Векторний його друг буде в асемблері щось типу:
movss xmm0, DWORD PTR __real@c0800000
movss xmm2, DWORD PTR __real@c0400000
movss xmm3, DWORD PTR __real@c0000000
movss xmm1, DWORD PTR __real@bf800000
unpcklps xmm3, xmm0
xorps xmm0, xmm0
unpcklps xmm1, xmm2
unpcklps xmm1, xmm3
movaps XMMWORD PTR tv129[esp+32], xmm0
movaps XMMWORD PTR _m$[esp+32], xmm1
addps xmm0, xmm1

На що тут потрібно звернути увагу? На те, що операція "скласти" (інструкція addps) дійсно одна, коли в першому прикладі їх 4, при цьому в першому прикладі так само багато сложебных пересилок даних туди-сюди. Дійсно, в сухому залишку інструкцій стало менше. Втім, у x86 час виконання (як і в інших CISC), не обмежена, в теорії можна в мікрокод навіть хоч модель всього світу запхати. Вважатися він буде нескінченно, але інструкція то одна! Однак, що набагато важливіше для нас саме зараз, це регістри xmm0-7 (0-15 в 64-розрядній архітектурі. Однак ми зараз не на схемотехніці і навіть не про асемблер говоримо, такі подробиці нам не важливі). Це спеціальні регістри з розширення SSE (англ. Streaming SIMD Extensions, потокове SIMD-розширення процесора) розмірністю 128 біт, кожний з яких може містити 4 значення з плаваючою точкою одинарної точності (32-х розрядні float), ну а так само всілякі комбінації із знакових і беззнакових цілих чисел різною розрядністю. Саме над ними і відбуваються всі SIMD-інструкції.
Це досить важливо, оскільки будь-яка операція має на увазі, що ви спочатку якимось чином дані завантажте туди. Тут і криється головна обмеження цієї технології — завантаження цих даних коштує не дуже дешево, тому змішана арифметика (векторні та скалярні операції упереміш) дадуть тільки великі витрати на перегоні даних туди-сюди, звідси всі алгоритми для векторних обчислень оптимізуються таким чином, щоб мінімізувати точки дотику, що не завжди просто.
Однак підключити сюди потокову обробку — продуктивність ще більше зростає за рахунок того, що DDR дозволяють одержати від 2-х (для ddr3 це аж 8) слів за одне звернення до пам'яті. Безумовно, процесор і скалярних обчисленнях спробує вдаватися до них, проте якщо йому сказати про це явно, йому простіше буде зрозуміти що і як завантажувати.
Як це працює всередині, ще один підводний камінь і verilog у статті js? Що далі? Квантова механіка?Всередині кожна з інструкцій виглядає приблизно так:
module adds(
input wire[0:31] a,
input wire[0:31] b,
input wire[0] size, // 8 bit or 16 bit
output reg[0:31] c,

input wire clk
);

завжди @ (posedge clk)
begin
case (size)
1'b0: begin
c[0:7] < = a[0:7] + b[0:7];
c[8:15] < = a[8:15] + b[8:15];
c[16:23] < = a[16:23] + b[16:23];
c[24:31] < = a[24:31] + b[24:31];
end
1'b1: begin
c[0:15] < = a[0:15] + b[0:15];
c[16:31] < = a[16:31] + b[16:31];
end
end

endmodule

Реальний приклад не наведу з двох причин: по-перше, вони всі дуже об'ємні, а до справи не відносяться, по-друге — ніхто з великих компаній, особливо intel, не особливо розігналися ним ділитися, а знайомих схемотехников звідти у мене немає (це тонкий натяк, до речі). Але це і не важливо. Важливо інше — подібні рядки коду (точніше, опису апаратури) є десь у надрах ядра процесора intel x86, доступного нам через мікроархітектуру у вигляді розширеного набору інструкцій sse. Ось тільки в інших архітектурах (так, ніби як, і всередині x86, десь під мікроархітектурою, там, де чесний RISC) він є частиною со-процесора з арифметики з плаваючою точкою. Якщо ж для x86 накладок на виклик інструкцій з-процесора особливо не накладається за можливості оптимізувати його всередині мікроархітектури, то в інших процесорах з-процесор найчастіше виконує окрему послідовність команд і управляється центральним. Таким чином, виклик його команди буде здійснено за схемою:
  • ініціювати параметри співпроцесора
  • передати значення, з якими йому необхідно працювати
  • встановити його внутрішній CP (лічильник інструкцій) на адресу програми, написаної для нього
  • запустити його
  • дочекатися, поки він завершить операцію (в цей час можна зайнятися чимось іншим, не менш корисним)
  • отримати результати
  • зупинити співпроцесор
Явно більше трьох інструкцій, які ми намагаємося скоротити. Звідси логіка роботи на специфічної апаратури полягає в тому, що знову ж таки необхідно мінімізувати точки дотику скалярної та векторної арифметики, щоб зменшити кількість пересилань даних та інших накладок.
Наприклад, опис ARM на вікіпедії:
Вдосконалений SIMD (NEON)
Розширення вдосконаленого SIMD, також зване технологією NEON — це комбінований 64 і 128-бітний набір команд SIMD (single instruction multiple data), який забезпечує стандартизований прискорення для медіадодатків і додатків обробки сигналу. NEON може виконувати декодування аудіоформату mp3 на частоті процесора в 10 МГц, і може працювати з мовним кодеком GSM AMR (adaptive multi-rate) на частоті більше 13МГц. Він володіє значним набором команд, окремими регістровими файлами, і незалежною системою виконання на апаратному рівні. NEON підтримує 8-, 16-, 32-, 64-бітну інформацію цілого типу, одинарної точності з плаваючою комою, і працює в операціях SIMD з обробки аудіо і відео (графіка та ігри). У NEON SIMD підтримує до 16 операцій одноразово.

Одним з недоліків (або, скажімо, особливістю) вдосконаленого SIMD є те, що співпроцесор виконує команди вдосконаленого SIMD з досить значною затримкою щодо коду основного процесора, затримка сягає двох десятків тактів і більше (залежить від архітектури і конкретних умов). З цієї причини при спробі основного процесора скористатися результатами обчислення співпроцесора виконання буде заморожено на значний час.
Взагалі, ми говоримо тут про SIMD в js. Так якого біса? ARM і x86 — це все, що у нас є, коли java-script-розробників хвилювало те, на якій архітектурі буде виконуватися їх код? Максимум — який навіть не браузер, а движок.
А от чорта з два. По-перше, а з якого переляку взагалі потягнули SIMD в JS? По-друге, спірне рішення, але їх зараз як грибів. А раз інструмент є, то розуміти як ним користуватися ефективно все-таки потрібно.
Практика
Отже, векторні операції скоро з'являться в js. Наскільки скоро? В даний момент їх підтримує firefox nightly, edge з прапором "експериментальні можливості" і chrome з прапором при запуску
--js-flags="--harmony-simd"
, тобто хоч у якомусь вигляді, але всі браузери. Крім цього є полифилл, так що можна використовувати вже прямо зараз.
Невеликий приклад як використовувати SIMD в js-код:
const someValue = SIMD.Float32x4(1,0,0,0);
const otherValue = SIMD.Float32x4(0,1,0,0);

const summ = SIMD.Float32x4.add(someValue, otherValue);

Повний список доступних функцій дивіться на MDN. Хочу звернути увагу, що SIMD.Float32x4 не є конструктором і запис
new SIMD.Float32x4(0, 0, 0, 0);
не є валидной.
Не буду розписувати всі можливості по використанню, їх не дуже багато, в цілому — арифметика та завантаження з вивантаженням даних, ще трохи прикладів все на тому ж MDN, відразу перейду до прикладів.
Позначення, запроваджені мною для простоти прикладів, такі:
const sload = SIMD.Float32x4.load;
const sadd = SIMD.Float32x4.add;
const smul = SIMD.Float32x4.mul;
const ssqrt = SIMD.Float32x4.sqrt;
const sstore = SIMD.Float32x4.store;

Згортка
Згорткою в математиці називають перетворення з одного набору даних в інший. Для програмістів згорткою є обхід всього масиву і виконання над кожним з його елементів математичного перетворення, наприклад, множення на інше число, додавання з іншим масивом або застосування оператора Собеля для зображення (він теж буде трохи нижче).
Скалярний варіант:

Векторний:

Тут і далі: по вертикальній осі дані, по горизонтальній — час. Помаранчевий блок — дані, над якими відбувається операція.
В коді це приклад зверху в явному вигляді, на js приблизно так (частина з алгоритму розрахунку дисперсії):
const arr = new Float32Array(someBuffer);

for (let i = 0; i < arr.length; i += 4) { // приклад передбачає, що розмір масиву кратний 4, будьте обережними з цим!
sstore(arr, i, smul(
sadd(
sload(arr, i),
expected // мат. очікування
),
sadd(
sload(arr, i),
expected // мат. очікування
)
));
}

Сума елементів масиву
Якщо не помиляюся, цей приклад йде першим у книзі з CUDA, та й взагалі є максимально типовим прикладом для SIMD (причому, в даному випадку якраз більше підходить для обчислення на відеокарті, доки дозволяє перейти до логарифмічної складності, ніж до векторних операцій).
Розповім многонитьевой алгоритм, який не складно адаптувати під векторний: спершу кожний парний (включаючи нульовий) підсумовується з непарною, наступним за ним. Потім нульовою (у якому тепер лежить сума a[0] + a[1]) підсумовується з другим парним (a[2], який дорівнює a[2] + a[3]) і так далі. На наступному кроці підсумовуються 0, 4, 8, ..., потім 0, 8, 16 і так далі по степенях двійки, поки масив не закінчиться.
У випадку з векторними операціями можна підсумувати перші 4 значення з другими 4-ма значеннями, в іншому — все так само, тобто
a[0-3] = a[0-3] + a[4-7];
a[8-11] = a[8-11] + a[12-15];
Графічно:
Скалярний варіант

Векторний:

Приріст швидкості очікується досить великий. Мій варіант на js:
const length = a.length;
let i = 0;
let k = 4;
while (k < size) {
for (i = 0; i < size; i += k * 2) {
SIMD.Float32x4.store(a, i, 
SIMD.Float32x4.add(
SIMD.Float32x4.load(a, i),
SIMD.Float32x4.load(a, i + k)
));
}
k = k << 1;
}

Цей приклад має обмеження: розмір масиву повинен бути ступенем двійки і кратні 4. В принципі, це не так складно виправити, але залишимо на домашнє завдання.
Матричне множення 4х4
Є сенс вважати, що матричні операції для квадратних матриць розміру 4 (а з ними і векторів такої ж розмірності. Математичних векторів, а не вычислительлных) SIMD дадуть помітний приріст. Що таке множення і як виглядає класичний варіант, а так само графічно — я не покажу (графічно виглядає ненаглядно, класичний варіант — використання математичного визначення множення матриць в лоб, його можна подивитися на вікіпедії), векторний код на js у мене вийшов такий:
let j = 0;
let row1, row2, row3, row4;
let brod1, brod2, brod3, brod4, row;
for (let i = 0; i < 10000; i++) {
c = new Float32Array(32);
row1 = SIMD.Float32x4.load(a, 0);
row2 = SIMD.Float32x4.load(a, 4);
row3 = SIMD.Float32x4.load(a, 8);
row4 = SIMD.Float32x4.load(a, 12);
for (j = 0; j < 4; j++) {
d = b[4 * j + 0];
brod1 = SIMD.Float32x4(d, d, d, d); // у нас немає аналога команди _mm_set1_ps
d = b[4 * j + 1];
brod2 = SIMD.Float32x4(d, d, d, d); // у нас немає аналога команди _mm_set1_ps
d = b[4 * j + 2];
brod3 = SIMD.Float32x4(d, d, d, d); // у нас немає аналога команди _mm_set1_ps
d = b[4 * j + 3];
brod4 = SIMD.Float32x4(d, d, d, d); // у нас немає аналога команди _mm_set1_ps
row = SIMD.Float32x4.add(
SIMD.Float32x4.add(
SIMD.Float32x4.mul(brod1, row1),
SIMD.Float32x4.mul(brod2, row2)
),
SIMD.Float32x4.add(
SIMD.Float32x4.mul(brod3, row4),
SIMD.Float32x4.mul(brod3, row4)
)
);
SIMD.Float32x4.store(c, j * 4, row);
}
}

Важкувато. Зауважу, що після перегляду коду, який скомпільовані c-шний варіант, не сильно засмутився недоліком _mm_set1_ps, доки всередині воно компілюється в 4 окремих команди завантаження, так що втрата не велика.
Оператор Собеля
Википедия. Є прикладом однієї з типових завдань, які повинні добре лягати на SIMD, доки є всередині згорткою над зображенням. Картинок, коду і алгоритму не буде, однак зауважу, що в тесті я кілька смухлевал. Справа в тому, що оператор Собеля оптимізується набагато більше і сильніше при використанні SIMD, проте спочатку потрібно перетворити дані до іншого виду, так і матан там не простий. Однак навіть просте застосування дало перевагу. Що стосується чесного перетворення — хоч jpeg і є дискретно-косинусным перетворенням, всередині він не так далеко пішов від згортки оператором Собеля, а саме за рахунок SSE (і старших) розширень процесора ми можемо дивитися відео у високій якості на процесор (і накладати набагато більш складні ефекти використовуючи відеокарту), так що даний алгоритм є прямим призначенням SIMD-інструкцій у процесорі, в тому числі, в js.
Майбутнє тут? Продуктивність
Тест руками можна запустити тут. Не забудьте переконатися, що ваш браузер підтримує SIMD. Вихідний код, не забуваємо про кнопочку fork, якщо є ідеї, як покращити і оптимізувати.
До тестів підійшов серйозно: кожен тест групується в 4 запуску, один з яких "холостий", призначений на прогрів кеша і щоб браузер зміг зібрати те, що він вважає за потрібне, в бінарний код. Три враховуються при обрахунку. Потім кожна з цих груп запускається за n разів, порядок вибирається випадковим чином, потім вважається математичне сподівання і дисперсія (чорна смужка на кожному з графіків).
У себе я запускав 500 разів кожну з груп, що дозволило виключити такі фактори як прибирання сміття, перемикання потік, замисленого касперського і інші оновлення windows 10, і відображає близькі до дійсності числа. На жаль, на js отримати більш точні висновки неможливо з тієї причини, що ми ніяк не можемо змусити планувальник завдань системи не перемикати завдання та інші речі, а отримати кількість тиків, відведених виключно під js, так само немає інструментів (є у вигляді профайлера, але він і без того занадто пекельний. Перевірити в ньому — посилання на тести вгорі є, можете проінспектувати).
Результати на моєму комп'ютері (core i5 6400m, firefox nightly 51.0 a):


Висновки
Як видно з імпровізованого порівняння на продуктивність, при хоч трохи більш-менш грамотному використанні технології вона дає перевагу в 20-40%. У специфічних завданнях можна вичавити і набагато більшу продуктивність, як наслідок — технологія варто того, щоб починати замислюватися про її використання. На жаль, векторне програмування досить складне, фахівців з нього мало, тим більше в світі js, тому всі і кожен не кинеться. Однак якщо ваш проект включає в себе складні обчислення — чутливий приріст швидкості зайвим не буде і може заощадити багато ресурсів і часу на вирішення інших завдань.
Зараз доступний полифилл і немає причин думати, що API для роботи з SIMD інструкцій у js зміниться, доки стандарт завершено на 90%, а решта 10%, по секрету, це роздуми про те, як би перевизначити операнди типу "+" і ".*". В js перевантаження операндів немає і не бачу причин тягти їх сюди, так що, швидше за все, у прямому вигляді увійде в стандарт, який після прийняття у найближчому ж релізі буде доступний в firefox і edge, google Chrome з них не поспішають з якоїсь причини, таким чином, якщо ви зараз (на момент написання статті, в серпні 2016 року) починаєте свою нову програму на JS, в якому буде безліч обчислень, варто відразу мати цю бібліотеку увазі і писати код якщо не відразу на ній (з полифиллом), то, принаймні, з огляду на ймовірну міграцію і передбачити можливість переписування, щоб в майбутньому не втратити багато часу на це.
Варта гра свічок або мої думки у вільній формі
Звичайно зростання продуктивності спостерігається, однак мені не дуже зрозумілий вибір технології і реалізація стандарту. Вона трохи більше, ніж повністю оптимізована тільки під x86, на будь-який інший архітектурі і розмір вектора може відрізнятися, і набір інструкцій (дуже поширена інструкція виду a + b * c, особливо у всіляких ДСП). Ясна річ, що який-небудь 66AK2x від TI (цифровий сигнальний процесор) ніхто в здоровому глузді не буде програмувати на javascript, однак супер-высокоуровневом платформонезависимом мовою дивно бачити платформозависимые інструкції. (Про це говорив Akon32
SIMD-інструкції? Навіщо? JS як би вважається мовою високого рівня, незалежним від заліза. А тут прямо в початковому тексті програми передбачається, що саме 4-компонентні вектора є в інструкціях процесора. А якщо тільки 3, або 5, або 8 компонентів? Невідомо, на чому код буде запускатися.
Імхо, більш далекоглядними (але не простими) рішеннями виглядають автоматична векторизація в JIT (або прямо у суперскалярному процесорі), або OpenCL/CUDA-подібний API.
Не знав, що коментарі при перенесенні в чернетки так само труться).
Моя особиста думка: набагато далекоглядніші, при цьому дозволяє компілятору робити теж саме, що і в цьому стандарті, є MPi (ну або OpenMP). Як би, наприклад, це виглядало:
function summ(a, b, N) {
const c = Float32Array(N);
let i;
/// #pragma omp parallel for shared(a, b, c) private(i)
for (i = 0; i < N; i++)
c[i] = a[i] + b[i];
}
}

Власне, плагіат з готового стандарту OpenMP. Це дозволить движку самому вибрати найбільш оптимальний варіант векторизації, оперувати не тільки 128-ми бітними векторами, так і зробить код платформонезависимым. Можна зробити це автоматично під час JIT? Не завжди вдається, а можливість вказати вручну була б зручною.
P. S. У минулій статті замість технології всі почали обговорювати математику і складні обчислення на javascript. Відповідний мова для цього чи ні, добре це чи погано, проте все більше і більше завдань на js вимагають великих математичних обчислень, а технологія, яка дозволяє прискорити їх, грає тільки на руку, ніж шкодить. Прошу утриматися від обговорення складних обчислень на js як таких.
Джерело: Хабрахабр

0 коментарів

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