Если вы – из тех программистов, которые в новогоднюю ночь пообещали себе писать более быстрый код, сегодня у вас есть шанс это обещание выполнить. Мы поговорим о том, как ускорить работу веб-решений с использованием технологии 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 в своих проектах?
Поделиться с друзьями
-->

Комментарии (33)


  1. Finom
    18.01.2017 13:10

    Как работать с DOM/BOM?


    1. staticlab
      18.01.2017 14:07

      Разговор с переводом, к сожалению.


    1. Shannon
      18.01.2017 15:30
      +1

      Сначала они доделают Минимально Жизнеспособный Продукт, а после этого займутся менее приоритетными фичами, к которым отнесли DOM — http://webassembly.org/docs/future-features/
      Пример игры http://webassembly.org/demo/

      Пока подразумевается что wasm объект подключается к js и работа с ним ничем не отличается от обычного js


  1. ARad
    18.01.2017 13:50

    ReferenceError: WebAssembly is not defined


    1. Shannon
      18.01.2017 15:19

      Нужно активировать в браузере поддержку
      В хроме открыть chrome://flags/#enable-webassembly
      В файрфоксе about:config и там найти javascript.options.wasm


    1. rumkin
      18.01.2017 16:54

      WebAssembly доступен только в ночных сборках.


      1. Shannon
        18.01.2017 17:00
        +1

        Уже давно в основной ветке и хрома и файрфокса под флагом, нужно только включить


        1. rumkin
          18.01.2017 19:48

          Включил, но все равно WebAssembly undefined.


          1. Shannon
            22.01.2017 00:32

            Если бы не было поддержки, то не было бы и флага который можно было бы включить
            В хроме 55 и файрфоксе 50 флаг включается и официальная wasm демка работает

            В хроме нужно браузер перезагрузить, в файрфоксе не надо


        1. DarthVictor
          21.01.2017 23:27

          В основной ветке Хрома я получил

          WebAssembly.compile(): Wasm decoding failedResult = expected version 0c 00 00 00, found 0d 00 00 00 @+4
          Нужна именно канарейка похоже.


          1. Shannon
            22.01.2017 00:22

            Все верно, для этого wasm (версия из статьи) нужна канарейка, а сам wasm в основной ветке работает нормально, потому что запускается эта демка
            http://webassembly.org/demo/


  1. tenbits
    18.01.2017 14:41
    +5

    Сразу бросились в глаза 2 ошибки производительности в fiboJsMemo


    • нельзя переопределять аргументы
    • нельзя динамически добавлять свойства к объекту

    Почему автор не написал этот вариант также как и си. Не умышленно ли часом?


    const memo = new Array(10000);
    function fiboJsMemoOpt (num) {
      if (num <= 1) return 1;
      const x = memo[num];
      if (x !== void 0) {
        return x;
      }
      return memo[num] = fiboJsMemoOpt(num - 1) + fiboJsMemoOpt(num - 2);
    }

    Js x 11,501,607 ops/sec ±2.53% (70 runs sampled)
    Js memoization x 465,102 ops/sec ±0.82% (86 runs sampled)
    Js memoization opt x 59,388,502 ops/sec ±2.89% (82 runs sampled)

    Главное, проценты у авторa получились красивые.


    1. staticlab
      18.01.2017 14:44
      +2

      Автору оригинала задали тот же вопрос.


    1. Shannon
      18.01.2017 16:08

      Согласен с вашим комментарием, только разве что оптимизация с void 0 уже не требуется, разницы между

        const x = memo[num];
        if (x !== void 0) {
          return x;
        }
      

      и

        if (memo[num]) 
          return memo[num];
        }
      

      никакой нет
      fiboJsMemoOptVar1: 8.424ms
      fiboJsMemoOptVar2: 7.873ms
      


    1. Shannon
      18.01.2017 16:26

      Перепроверил, показалось странным, так как обе функции на текущем v8 движке должны работать одинаково, обе они успешно оптимизируются и странно, что такая большая разница между вашей функций и из топика.

      В итоге нужна 1 единственная маленькая корректировка в оригинальную функцию и разница практически не будет

      fiboJsMemoOpt: 77.259ms
      fiboJsMemoMyMods: 88.285ms

      В оригинале вместо

      memo = memo || {};
      

      надо написать (что в данном случае намного правильнее чем {})

      memo = memo || [];
      

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


    1. selgjos
      18.01.2017 18:40
      +2

      Самое интересное то, что смотреть на результаты эти не имеет смысла. Во-первых си-код не валиден, ибо всегда будет возвращать ноль. Во-вторых что измеряет memoization? Ничего. Чтение значения из памяти.

      Почему автор не написал этот вариант также как и си. Не умышленно ли часом?

      Ну вы нашли типичную причину проблем жаваскрипта — непредсказуемость. Почему не написал? Спорный вопрос, но факт есть факт — никаких причин к замдлению в 100раз нет.

      Главное, проценты у авторa получились красивые.

      Результаты васма вы не показали. Да и это ничего не меняет, только то, что васм не особо отличается от ЖС и его нативность лишь миф. Результаты в 60кк вызовов(в секунду) функций с одним обращением к «памяти» просто смешны.

      А так же важно то, что ни цикл с одной операцией, ни обращение к памяти — ни есть то, что имеет смысл сравнивать. Наиболее близкая к реальности реализация — это рекурсия. Ведь большинство кода на том же жс есть не более чем вызов примитивных функций в гигантских кол-вах.


  1. pythonproof
    18.01.2017 14:41

    А если надо получить число фибонначи при n = 10001?


    1. Keyten
      18.01.2017 15:12
      +1

      То можно сделать так:


      где — золотое сечение.

      Но пост же не об этом.


  1. Andrey_Volk
    18.01.2017 14:43

    Я конечно не специалист, но почему C, C++, Rust, Swift — низкоуровневые языки?


    1. greabock
      23.01.2017 00:15
      +1

      В сравнении с JS, конечно же.


  1. Varim
    18.01.2017 14:49

    return memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
    

    что бы мемоизация работала, разве не надо так
    fibonacciMemo(n - 2) + fibonacciMemo(n - 1);
    
    разве сначала не левый, а затем правый операнд вызывается?


  1. Varim
    18.01.2017 15:01

    На этой странице, кстати, вы можете попробовать всё сами.
    Я получаю ошибку в консоли:
    bundle.js:66 Uncaught (in promise) ReferenceError: WebAssembly is not defined
    at fetch.then.then.buffer (bundle.js:66)


    1. Shannon
      18.01.2017 15:20
      +1

      Нужно активировать в браузере поддержку
      В хроме открыть chrome://flags/#enable-webassembly
      В файрфоксе about:config и там найти javascript.options.wasm
      В edge и safari тоже есть поддержка, не знаю как там включается


  1. DenimTornado
    18.01.2017 17:24
    +2

    Ха-ха, Intel HD300 не умеет в WebGL. Вот и дожил до момента, когда мой мак не тянет что-то в сети.


    1. staticlab
      18.01.2017 18:04

      Казалось бы, причём тут WebGL?


      1. DenimTornado
        18.01.2017 20:33
        +1

        Примерно при этом — http://take.ms/QjATe В хром флагах включены и WebGL и WebAssembly само собой.


    1. DenimTornado
      18.01.2017 20:45
      +7

      Нда, Хабра в своём репертуаре…

      Резюме: Пытался запустить демку из первого коммента. Хром со всеми включёнными флагами ругается на то, что нет поддержки WebGL. На Хром экспериментс пишут, что видеокарта не поддерживает WebGL. В итоге Лиса спокойно проглотила и запустила демку. Но на абсолютно безобидный коммент сразу и в карму, и сюда. Люди, что с вами не так? Вы что что все в тех. поддержке переработали? Я понимаю, что Хабре от меня несильно много пользы, но вот с такой реакцией пользы и не захочется приносить.


      1. 3aicheg
        19.01.2017 06:09
        -6

        Не нойте.


        1. DenimTornado
          19.01.2017 11:25
          +3

          Сказал человек с кармой -16. Вам ваш совет не помог судя по всему.


          1. 3aicheg
            19.01.2017 14:25
            -4

            Лол. У вас карма головного мозга?


  1. mark_ablov
    19.01.2017 07:14

    Как часто вы считаете числа Фибоначчи в JS'e?
    Тест очень синтетический и не показывает абсолютно ничего.


  1. orcy
    19.01.2017 19:44

    Интересно еще сравнить с pnacl.


  1. Px2
    23.01.2017 10:39
    +3

    Как убить интерес к теме? Начните с чисел Фибоначчи.