WebAssembly – шлях до нових горизонтів продуктивності

Якщо ви – з тих програмістів, які у новорічну ніч пообіцяли собі писати більш швидкий код, сьогодні у вас є шанс цю обіцянку виконати. Ми поговоримо про те, як прискорити роботу веб-рішень з використанням технології WebAssembly (скорочено її називають wasm). Технологія це дуже молода, зараз – пора її становлення, однак, вона цілком може мати серйозний вплив на майбутнє розробки для інтернету.

image
Тут я розповім про те, як створювати модулі WebAssembly, як з ними працювати, як викликати їх з клієнтського коду в браузері так, ніби це модулі, написані на PHP. Ми розглянемо два набору реалізацій алгоритма пошуку чисел Фібоначчі. Один з них представлений звичайними JavaScript-функціями, другий – написаний на C і перетворений в модуль WebAssembly. Це дозволить порівняти продуктивність wasm і JS при вирішенні схожих завдань.

Код для випробувань
Ми будемо досліджувати три підходи до пошуку чисел Фібоначчі. Перший використовує цикл. Другий задіює рекурсію. Третій заснований на техніці мемоизации. Всі вони реалізовані на JavaScript і на C.

Ось JS-код:

function fiboJs(num){
var a = 1, b = 0, temp;

while (num >= 0){
temp = a;
a = a + b;
b = temp;
num--;
}

return b;
}

const fiboJsRec = (num) => {
if (num <= 1) return 1;

return fiboJsRec(num - 1) + fiboJsRec(num - 2);
}

const fiboJsMemo = (num, memo) => {
memo = memo || {};

if (memo[num]) return memo[num];
if (num <= 1) return 1;

return memo[num] = fiboJsMemo(num - 1, memo) + fiboJsMemo(num - 2, приміток);
}

module.exports = {fiboJs, fiboJsRec, fiboJsMemo};

Ось – те ж саме, написаний на C:

int fibonacci(int n) {
int a = 1;
int b = 1;

while (n-- > 1) {
int t = a;
a = b;
b += t;
}

return b;
}

int fibonacciRec(int num) {
if (num <= 1) return 1;

return fibonacciRec(num - 1) + fibonacciRec(num - 2);
}

int memo[10000];

int fibonacciMemo(int n) {
if (memo[n] != -1) return memo[n];

if (n == 1 || n == 2) {
return 1;
} else {
return memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
}
}

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

Технології
Технологія WebAssembly – це ініціатива, спрямована на створення безпечного, стерпного і для швидкого завантаження і виконання формату коду, відповідного для Web. WebAssembly – це не мова програмування. Це – мета компіляції, у якої є специфікації текстового та бінарного форматів. Це означає, що інші низькорівневі мови, такі, як C/C++, Rust, Swift, і так далі, можна скомпілювати в WebAssembly. WebAssembly дає доступ до тих же API, що і браузерний JavaScript, органічно вбудовується в існуючий стек технологій. Це відрізняє wasm від чогось на кшталт Java-аплетів. Архітектура WebAssembly – це результат колективної роботи співтовариства, в якому є представники всіх провідних розробників веб-браузерів. Для компіляції коду в формат WebAssembly використовується Emscripten.

Emscripten – це компілятор з байт-коду LLVM в JavaScript. Тобто, з його допомогою можна скомпілювати в JavaScript програми, написані на C/C++ чи інших мовних, код на яких можна перетворити у формат LLVM. Emscripten надає набір API для портування коду в формат, який підходить для веб. Цим проектом вже багато років, в основному його використовують для перетворення ігор у їх браузерні варіанти. Emscripten дозволяє досягти високої продуктивності завдяки тому, що він генерує код, відповідний стандартам Asm.js, про яку нижче, але нещодавно його успішно оснастили підтримкою WebAssembly.

Asm.js – це низькорівневе оптимізоване підмножина JavaScript, забезпечує лінійний доступ до пам'яті з допомогою типізованих масивів і підтримує анотації з інформацією про типи даних. Asm.js дозволяє підвищити продуктивність рішень. Це – теж не нову мову програмування, тому, якщо браузер його не підтримує, Asm.js-код буде виконуватися як звичайний JavaScript, тобто від його використання не вдасться отримати приросту продуктивності.

WebAssembly, станом на 10.01.2017, підтримується Chrome Canary і Firefox. Для того, щоб wasm-код запрацював, потрібно активувати відповідну можливість в налаштуваннях. Safari підтримка WebAssembly поки в стадії розробки. У V8 wasm включений за замовчуванням.
Ось цікаве відео про движку V8, про поточний стан підтримки JavaScript і WebAssembly c Chrome Dev Summit 2016.

Збірка і завантаження модуля
Займемося перетворенням програми, написаної на C, формат wasm. Для того, щоб це зробити, я вирішив скористатися можливістю створення автих модулів WebAssembly. При такому підході на виході компілятора ми отримуємо тільки файл з кодом WebAssembly, без додаткових допоміжних .js-файлів.

Такий підхід заснований на концепції додаткових модулів (side module) Emscripten. Тут має сенс використовувати подібні модулі, так як вони, в суті, дуже схожі на динамічні бібліотеки. Наприклад, системні бібліотеки не підключаються до них автоматично, вони являють собою якісь самодостатні блоки коду, що видається компілятором.

$ emcc fibonacci.c -Os -s WASM=1 -s SIDE_MODULE=1 -o fibonacci.wasm

Після отримання бінарного файлу нам потрібно лише завантажити його в браузер. Для того, щоб це зробити, API WebAssembly надає об'єкт верхнього рівня WebAssembly, який містить методи, потрібні для того, щоб скомпілювати, создать екземпляр модуля. Ось простий метод, заснований на gist Алона Закаи, який працює як універсальний завантажувач.

module.exports = (filename) => {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
const imports = {
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256
}),
table: new WebAssembly.Table({
initial: 0,
element: 'anyfunc'
})
}
};

return new WebAssembly.Instance(module, imports);
});
}

Найприємніше тут те, що все відбувається асинхронно. Спочатку ми беремо вміст файлу і конвертуємо його в структуру даних формату ArrayBuffer. Буфер містить вихідні двійкові дані фіксованої довжини. Безпосередньо виконувати їх ми не можемо, саме тому на наступному кроці буфер передають методом WebAssembly.compile, який повертає WebAssembly.Module, примірник якого, в результаті, можна створити з допомогою WebAssembly.Instance.

Почитайте опис двійкового формата, який використовує WebAssembly, якщо хочете глибше у всьому цьому розібратися.

Тестування продуктивності
Настав час поглянути на те, як використовувати wasm-модуль, як протестувати його продуктивність і порівняти її з швидкістю роботи JavaScript. На вхід досліджуваних функцій будемо подавати число 40. Ось код тестів:

const Benchmark = require('benchmark');
const loadModule = require('./loader');
const {fiboJs, fiboJsRec, fiboJsMemo} = require('./fibo.js');
const suite = new Benchmark.Suite;
const numToFibo = 40;

window.Benchmark = Benchmark; //Benchmark.js uses the global object internally

console.info('Benchmark started');

loadModule('fibonacci.wasm').then(instance => {
const fiboNative = instance.exports._fibonacci;
const fiboNativeRec = instance.exports._fibonacciRec;
const fiboNativeMemo = instance.exports._fibonacciMemo;

suite
.add('Js', () => fiboJs(numToFibo))
.add('Js recursive', () => fiboJsRec(numToFibo))
.add('Js memoization', () => fiboJsMemo(numToFibo))
.add('Native', () => fiboNative(numToFibo))
.add('Native recursive', () => fiboNativeRec(numToFibo))
.add('Native memoization', () => fiboNativeMemo(numToFibo))
.on('cycle', (event) => console.log(String(event.target)))
.on('complete', function() {
console.log('Fastest:' + this.filter('швидко').map('name'));
console.log('Slowest:' + this.filter('slowest').map('name'));
console.info('Benchmark finished');
})
.run({ 'async': true });
});

А ось результати. цій сторінці, до речі, ви можете спробувати все самі.

JS loop x 8,605,838 ops/sec ±1.17% (55 runs sampled)
JS recursive x 0.65 ops/sec ±1.09% (6 runs sampled)
JS memoization x 407,714 ops/sec ±0.95% (59 runs sampled)
Native loop x 11,166,298 ops/sec ±1.18% (54 runs sampled)
Native recursive x 2.20 ops/sec ±1.58% (10 runs sampled)
Native memoization x 30,886,062 ops/sec ±1.64% (56 runs sampled)
Fastest: Native memoization
Slowest: JS recursive

Добре помітно, що wasm-код, отриманий з програми на C (у виведенні тесту він позначений як «Native») швидше ніж аналогічний код, написаний на звичайному JavaScript («JS» у виведенні тесту). При цьому самою швидкою реалізацією виявилася wasm-функція пошуку чисел Фібоначчі, застосовує техніку мемоизации, а найповільнішою – рекурсивна функція на JavaScript.

Якщо посидіти над отриманими результатами з калькулятором, можна з'ясувати наступне:

  • Краща по продуктивності реалізація на C на 375% швидше, ніж найкраща реалізація на JS.

  • найшвидший варіант на C використовує мемоизацию. На JS – це реалізація алгоритму з використанням циклу.

  • Друга по продуктивності реалізація на C все одно швидше, ніж найшвидший варіант на JS.

  • Сама повільна реалізація алгоритму на C на 338% швидше, ніж самий повільний варіант на JS.
Підсумки
Сподіваюся, вам сподобався мій короткий розповідь про можливості WebAssembly, і про те, чого можна досягти з допомогою цієї технології вже сьогодні. За рамками даного матеріалу залишилося чимало тем, серед яких – wasm-модулі при компіляції яких створюються і допоміжні файли, різні способи взаємодії між скомпільованим кодом на C і кодом на JS, динамічне зв'язування. Цілком можливо, що ми з вами їх коли-небудь обговоримо. Тепер у вас є все необхідне для початку експериментів з WebAssembly. До речі, можете ще поглянути на офіційне керівництво для розробників WebAssembly.

Для того, щоб бути у курсі останніх подій в області wasm, додайте в закладки цю сторінку з відомостями про досягнення і плани розвитку проекту. Корисно буде заглядати і в журнал Emscripten.

До речі, а ви вже думали про те, як скористатися можливостями WebAssembly у своїх проектах?
Джерело: Хабрахабр

0 коментарів

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