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

в 9:59, , рубрики: javascript, webassembly, Блог компании RUVDS.com, Веб-разработка, Разработка веб-сайтов

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

image


Здесь я расскажу о том, как создавать модули WebAssembly, как с ними работать, как вызывать их из клиентского кода в браузере так, будто это модули, написанные на JS. Мы рассмотрим два набора реализаций алгоритма поиска чисел Фибоначчи. Один из них представлен обычными 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, memo);
}

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('fastest').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 в своих проектах?

Автор: RUVDS.com

Источник

Поделиться

* - обязательные к заполнению поля