- PVSM.RU - https://www.pvsm.ru -

WebAssembly – путь к новым горизонтам производительности

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

image


Здесь я расскажу о том, как создавать модули WebAssembly, как с ними работать, как вызывать их из клиентского кода в браузере так, будто это модули, написанные на JS. Мы рассмотрим два набора реализаций алгоритма [1] поиска чисел Фибоначчи. Один из них представлен обычными JavaScript-функциями, второй – написан на C и преобразован в модуль WebAssembly. Это позволит сравнить производительность wasm и JS при решении схожих задач.

Код для испытаний

Мы будем исследовать три подхода к поиску чисел Фибоначчи. Первый использует цикл. Второй задействует рекурсию. Третий основан на технике мемоизации. Все они реализованы на JavaScript и на C.

Вот [2] 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, memo);
}

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

Вот [3] – то же самое, написанное на 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);
  }
}

Тонкости реализации обсуждать здесь не будем, всё же, наша основная цель в другом. Если хотите, здесь [4] можете почитать о числах Фибоначчи, вот [5] – интересное обсуждение рекурсивного подхода к поиску этих чисел, а вот [6] – материал про мемоизацию. Прежде чем переходить к практическому примеру, остановимся ненадолго на особенностях технологий, имеющих отношение к нашему разговору.

Технологии

Технология WebAssembly – это инициатива, направленная на создание безопасного, переносимого и быстрого для загрузки и исполнения формата кода, подходящего для Web. WebAssembly – это не язык программирования. Это – цель компиляции, у которой имеются спецификации текстового и бинарного форматов. Это означает, что другие низкоуровневые языки, такие, как C/C++, Rust, Swift, и так далее, можно скомпилировать в WebAssembly. WebAssembly даёт доступ к тем же API, что и браузерный JavaScript, органично встраивается в существующий стек технологий. Это отличает wasm от чего-то вроде Java-апплетов [7]. Архитектура [8] 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 [9] и Firefox [10]. Для того, чтобы wasm-код заработал, нужно активировать соответствующую возможность в настройках. В Safari [11] поддержка WebAssembly пока в стадии разработки. В V8 wasm включён [12] по умолчанию.
Вот [13] интересное видео о движке V8, о текущем состоянии поддержки JavaScript и WebAssembly c Chrome Dev Summit 2016 [14].

Сборка и загрузка модуля

Займёмся преобразованием программы, написанной на C, в формат wasm [15]. Для того, чтобы это сделать, я решил воспользоваться возможностью создания автономн [16]ых модулей WebAssembly. При таком подходе на выходе компилятора мы получаем только файл с кодом WebAssembly, без дополнительных вспомогательных .js-файлов.

Такой подход основан на концепции дополнительных модулей [17] (side module) Emscripten. Здесь имеет смысл использовать подобные модули, так как они, в сущности, очень похожи на динамические библиотеки. Например, системные библиотеки не подключаются к ним автоматически, они представляют собой некие самодостаточные блоки кода, выдаваемого компилятором.

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

После получения бинарного файла нам нужно лишь загрузить его в браузер. Для того, чтобы это сделать, API WebAssembly [18] предоставляет объект верхнего уровня WebAssembly, который содержит методы, нужные для того, чтобы скомпилировать [19] и создать [20] экземпляр модуля. Вот [21] простой метод, основанный на gist [22] Алона Закаи, который работает как универсальный загрузчик.

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 [23]. Буфер содержит исходные двоичные данные фиксированной длины. Напрямую исполнять их мы не можем, именно поэтому на следующем шаге буфер передают методу WebAssembly.compile, который возвращает WebAssembly.Module [24], экземпляр которого, в итоге, можно создать с помощью WebAssembly.Instance.

Почитайте описание двоичного формат [25]а, который использует WebAssembly, если хотите глубже во всём этом разобраться.

Тестирование производительности

Пришло время взглянуть на то, как использовать wasm-модуль, как протестировать его производительность и сравнить её со скоростью работы JavaScript. На вход исследуемых функций будем подавать число 40. Вот [26] код тестов:

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('fastest').map('name'));
    console.log('Slowest: ' + this.filter('slowest').map('name'));
    console.info('Benchmark finished');
  })
  .run({ 'async': true });
});

А вот – результаты. На этой странице [27], кстати, вы можете попробовать всё сами.

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. Кстати, можете ещё взглянуть на официальное руководство для разработчиков [28] WebAssembly.

Для того, чтобы быть в курсе последних событий в области wasm, добавьте в закладки эту страницу [29] со сведениями о достижениях и планах развития проекта. Полезно будет заглядывать и в журнал изменений [30] Emscripten.

Кстати, а вы уже думали о том, как воспользоваться возможностями WebAssembly в своих проектах?

Автор: RUVDS.com

Источник [31]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/234797

Ссылки в тексте:

[1] алгоритма: https://medium.com/developers-writing/fibonacci-sequence-algorithm-in-javascript-b253dc7e320e

[2] Вот: https://gist.github.com/zzarcon/f25863252278b22e79f2cebaf11bb9da#file-fibo-js

[3] Вот: https://gist.github.com/zzarcon/4b8cd1c3b686f81e56c554b96dfc9600#file-fibonacci-c

[4] здесь: https://en.wikipedia.org/wiki/Fibonacci_number

[5] вот: http://stackoverflow.com/questions/8965006/java-recursive-fibonacci-sequence

[6] вот: https://en.wikipedia.org/wiki/Memoization

[7] Java-апплетов: https://en.wikipedia.org/wiki/Java_applet

[8] Архитектура: https://github.com/WebAssembly/design

[9] Chrome Canary: https://www.chromestatus.com/features/5453022515691520

[10] Firefox: https://hacks.mozilla.org/2016/03/a-webassembly-milestone/

[11] Safari: https://webkit.org/status/#specification-webassembly

[12] включён: https://chromium.googlesource.com/v8/v8/+/34b63f050b1a247bb64ddc91c967501ce04e011f

[13] Вот: https://www.youtube.com/watch?v=PvZdTZ1Nl5o

[14] Chrome Dev Summit 2016: https://www.youtube.com/playlist?list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj

[15] wasm: http://webassembly.org/docs/semantics/

[16] автономн: https://github.com/kripken/emscripten/wiki/WebAssembly-Standalone

[17] дополнительных модулей: https://github.com/kripken/emscripten/wiki/Linking

[18] API WebAssembly: https://github.com/WebAssembly/design/blob/master/JS.md

[19] скомпилировать: https://github.com/WebAssembly/design/blob/master/JS.md#webassemblycompile

[20] создать: https://github.com/WebAssembly/design/blob/master/JS.md#webassemblyinstance-constructor

[21] Вот: https://gist.github.com/zzarcon/f41cb1fddaff4097c216bf38339a296b#file-loader-js

[22] gist: https://gist.github.com/kripken/59c67556dc03bb6d57052fedef1e61ab

[23] ArrayBuffer: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer

[24] WebAssembly.Module: https://github.com/WebAssembly/design/blob/master/JS.md#webassemblymodule-constructor

[25] двоичного формат: https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md

[26] Вот: https://gist.github.com/zzarcon/e0deeff77a365ea216294db0090632fe#file-benchmark-js

[27] этой странице: https://zzarcon.github.io/WebAssembly-demo/

[28] руководство для разработчиков: http://webassembly.org/getting-started/developers-guide/

[29] эту страницу: http://webassembly.org/roadmap/

[30] журнал изменений: https://github.com/kripken/emscripten/blob/master/ChangeLog.markdown

[31] Источник: https://habrahabr.ru/post/319834/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best