Замена горячего пути в JavaScript вашего приложения на WebAssembly

Это стабильно быстро, йоу.

В своих предыдущих статьях я рассказывал о том, как WebAssembly позволяет перенести библиотечную экосистему C/C++ в Интернет. Одним из приложений, широко использующих библиотеки C/C++, является squoosh , наше веб-приложение, которое позволяет сжимать изображения с помощью различных кодеков, скомпилированных из C++ в WebAssembly.

WebAssembly — это виртуальная машина низкого уровня, выполняющая байт-код, хранящийся в файлах .wasm . Этот байт-код строго типизирован и структурирован таким образом, что его можно скомпилировать и оптимизировать для хост-системы гораздо быстрее, чем это может сделать JavaScript. WebAssembly предоставляет среду для запуска кода, в котором с самого начала учитывалась изолированная программная среда и встраивание.

По моему опыту, большинство проблем с производительностью в Интернете вызвано принудительной компоновкой и чрезмерным рисованием, но время от времени приложению приходится выполнять вычислительно затратную задачу, которая отнимает много времени. WebAssembly может помочь здесь.

Горячий путь

В squoosh мы написали функцию JavaScript , которая поворачивает буфер изображения на угол, кратный 90 градусам. Хотя OffscreenCanvas был бы идеальным для этого, он не поддерживается в целевых браузерах и немного глючит в Chrome .

Эта функция перебирает каждый пиксель входного изображения и копирует его в другую позицию выходного изображения для достижения поворота. Для изображения размером 4094х4096 пикселей (16 мегапикселей) потребуется более 16 миллионов итераций внутреннего блока кода, что мы называем «горячим путем». Несмотря на такое довольно большое количество итераций, два из трёх протестированных нами браузеров выполняют задачу за 2 секунды или меньше. Приемлемая продолжительность для такого типа взаимодействия.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Однако один браузер занимает более 8 секунд. Способ, которым браузеры оптимизируют JavaScript, действительно сложен , и разные движки оптимизируют разные вещи. Некоторые оптимизируют необработанное выполнение, некоторые оптимизируют взаимодействие с DOM. В данном случае мы попали по неоптимизированному пути в одном браузере.

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

WebAssembly для предсказуемой производительности

В целом, JavaScript и WebAssembly могут достичь одинаковой максимальной производительности. Однако для JavaScript такой производительности можно достичь только «быстрым путем», и зачастую сложно оставаться на этом «быстром пути». Одним из ключевых преимуществ WebAssembly является предсказуемая производительность даже в разных браузерах. Строгая типизация и низкоуровневая архитектура позволяют компилятору обеспечивать более строгие гарантии, что код WebAssembly нужно оптимизировать только один раз и он всегда будет использовать «быстрый путь».

Написание для WebAssembly

Ранее мы взяли библиотеки C/C++ и скомпилировали их в WebAssembly, чтобы использовать их функциональность в Интернете. Мы особо не трогали код библиотек, а просто написали небольшие фрагменты кода на C/C++, чтобы сформировать мост между браузером и библиотекой. На этот раз наша мотивация другая: мы хотим написать что-то с нуля, имея в виду WebAssembly, чтобы можно было использовать преимущества WebAssembly.

Архитектура веб-сборки

При написании для WebAssembly полезно немного больше понять, что такое WebAssembly на самом деле.

Цитирую WebAssembly.org :

Когда вы компилируете фрагмент кода C или Rust в WebAssembly, вы получаете файл .wasm , содержащий объявление модуля. Это объявление состоит из списка «импорта», который модуль ожидает от своей среды, списка экспорта, который этот модуль делает доступным хосту (функции, константы, фрагменты памяти) и, конечно же, фактических двоичных инструкций для функций, содержащихся внутри. .

Кое-что я не осознавал, пока не изучил это: стек, который делает WebAssembly «виртуальной машиной на основе стека», не хранится в том фрагменте памяти, который используют модули WebAssembly. Стек полностью является внутренним и недоступен веб-разработчикам (кроме DevTools). Таким образом, можно писать модули WebAssembly, которым вообще не нужна дополнительная память и которые используют только внутренний стек виртуальной машины.

В нашем случае нам нужно будет использовать некоторую дополнительную память, чтобы обеспечить произвольный доступ к пикселям нашего изображения и создать повернутую версию этого изображения. Для этого и нужен WebAssembly.Memory .

Управление памятью

Обычно, как только вы используете дополнительную память, вам необходимо каким-то образом управлять этой памятью. Какие части памяти используются? Какие из них бесплатны? В C, например, есть функция malloc(n) , которая находит пространство памяти из n последовательных байтов. Функции такого типа еще называют «распределителями». Конечно, реализация используемого распределителя должна быть включена в ваш модуль WebAssembly и увеличит размер вашего файла. Размер и производительность этих функций управления памятью могут существенно различаться в зависимости от используемого алгоритма, поэтому многие языки предлагают на выбор несколько реализаций («dmalloc», «emmalloc», «wee_alloc» и т. д.).

В нашем случае мы знаем размеры входного изображения (и, следовательно, размеры выходного изображения) до запуска модуля WebAssembly. Здесь мы увидели возможность: традиционно мы передавали буфер RGBA входного изображения в качестве параметра функции WebAssembly и возвращали повернутое изображение в качестве возвращаемого значения. Чтобы сгенерировать это возвращаемое значение, нам придется использовать распределитель. Но поскольку мы знаем общий объем необходимой памяти (в два раза больше размера входного изображения, один раз для ввода и один раз для вывода), мы можем поместить входное изображение в память WebAssembly с помощью JavaScript , запустить модуль WebAssembly для генерации второго, повернутое изображение, а затем используйте JavaScript, чтобы прочитать результат. Мы можем обойтись вообще без использования какого-либо управления памятью!

Избалован выбором

Если вы посмотрите на исходную функцию JavaScript , которую мы хотим использовать в WebAssembly, то увидите, что это чисто вычислительный код без API-интерфейсов, специфичных для JavaScript. Таким образом, портировать этот код на любой язык должно быть довольно просто. Мы оценили три разных языка, которые компилируются в WebAssembly: C/C++, Rust и AssemblyScript. Единственный вопрос, на который нам нужно ответить для каждого из языков: как получить доступ к необработанной памяти без использования функций управления памятью?

С и Эмскриптен

Emscripten — это компилятор C для цели WebAssembly. Цель Emscripten — служить заменой широко известных компиляторов C, таких как GCC или clang, и в основном совместим с флагами. Это основная часть миссии Emscripten, поскольку компания хочет максимально упростить компиляцию существующего кода C и C++ в WebAssembly.

Доступ к необработанной памяти заложен в самой природе C, и указатели существуют именно по этой причине:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Здесь мы превращаем число 0x124 в указатель на беззнаковые 8-битные целые числа (или байты). Это фактически превращает переменную ptr в массив, начинающийся с адреса памяти 0x124 , который мы можем использовать как любой другой массив, позволяя нам получать доступ к отдельным байтам для чтения и записи. В нашем случае мы смотрим на буфер RGBA изображения, порядок которого мы хотим изменить для достижения вращения. Чтобы переместить пиксель, нам фактически нужно переместить одновременно 4 последовательных байта (по одному байту на каждый канал: R, G, B и A). Чтобы упростить задачу, мы можем создать массив 32-битных целых чисел без знака. По соглашению, наше входное изображение начинается с адреса 4, а выходное изображение начинается сразу после окончания входного изображения:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

После переноса всей функции JavaScript на C мы можем скомпилировать файл C с помощью emcc :

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Как всегда, emscripten генерирует файл связующего кода под названием c.js и модуль wasm под названием c.wasm . Обратите внимание, что модуль Wasm сжимается с помощью gzip всего до 260 байт, а код соединения после gzip занимает около 3,5 КБ. После некоторых усилий нам удалось отказаться от связующего кода и создать экземпляры модулей WebAssembly с помощью ванильных API. Это часто возможно с помощью Emscripten, если вы не используете ничего из стандартной библиотеки C.

Ржавчина

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

Одним из таких инструментов является wasm-pack , созданный рабочей группой Rustwasm . wasm-pack берет ваш код и превращает его в веб-модуль, который готово работать с такими сборщиками, как webpack. wasm-pack — чрезвычайно удобный инструмент, но на данный момент он работает только с Rust. Группа рассматривает возможность добавления поддержки других языков, ориентированных на WebAssembly.

В Rust срезы — это то же самое, что массивы в C. И, как и в C, нам нужно создавать срезы, которые используют наши начальные адреса. Это противоречит модели безопасности памяти, которую применяет Rust, поэтому, чтобы добиться своего, нам приходится использовать ключевое слово unsafe , позволяющее нам писать код, не соответствующий этой модели.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Компиляция файлов Rust с использованием

$ wasm-pack build

дает модуль Wasm размером 7,6 КБ с примерно 100 байтами связующего кода (оба после gzip).

Ассемблерскрипт

AssemblyScript — довольно молодой проект, целью которого является компилятор TypeScript в WebAssembly. Однако важно отметить, что он не будет просто использовать TypeScript. AssemblyScript использует тот же синтаксис, что и TypeScript, но заменяет стандартную библиотеку своей собственной. Их стандартная библиотека моделирует возможности WebAssembly. Это означает, что вы не можете просто скомпилировать любой TypeScript, который у вас есть, в WebAssembly, но это означает , что вам не нужно изучать новый язык программирования, чтобы писать WebAssembly!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Учитывая небольшой размер шрифта, который имеет наша функция rotate() , перенести этот код на AssemblyScript было довольно легко. Функции load<T>(ptr: usize) и store<T>(ptr: usize, value: T) предоставляются AssemblyScript для доступа к необработанной памяти. Чтобы скомпилировать наш файл AssemblyScript , нам нужно всего лишь установить пакет npm AssemblyScript/assemblyscript и запустить

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript предоставит нам модуль Wasm размером около 300 байт без связующего кода. Модуль работает только с ванильными API WebAssembly.

Экспертиза WebAssembly

Размер Rust в 7,6 КБ на удивление велик по сравнению с двумя другими языками. В экосистеме WebAssembly есть несколько инструментов, которые могут помочь вам проанализировать ваши файлы WebAssembly (независимо от языка, на котором они были созданы) и рассказать вам, что происходит, а также помочь вам улучшить вашу ситуацию.

Твигги

Twiggy — еще один инструмент от команды Rust WebAssembly, который извлекает кучу полезных данных из модуля WebAssembly. Этот инструмент не является специфичным для Rust и позволяет вам проверять такие вещи, как граф вызовов модуля, определять неиспользуемые или лишние разделы и выяснять, какие разделы вносят вклад в общий размер файла вашего модуля. Последнее можно сделать с помощью top команды Твигги:

$ twiggy top rotate_bg.wasm
Скриншот установки Twiggy

В этом случае мы видим, что большая часть размера нашего файла определяется распределителем. Это было удивительно, поскольку наш код не использует динамическое распределение. Еще одним важным фактором является подраздел «имена функций».

васм-стрип

wasm-strip — инструмент из WebAssembly Binary Toolkit , или сокращенно wabt. Он содержит несколько инструментов, которые позволяют проверять модули WebAssembly и манипулировать ими. wasm2wat — это дизассемблер, который преобразует двоичный модуль Wasm в удобочитаемый формат. Wabt также содержит wat2wasm , который позволяет вам превратить этот удобочитаемый формат обратно в двоичный модуль Wasm. Хотя мы использовали эти два взаимодополняющих инструмента для проверки файлов WebAssembly, мы обнаружили, что wasm-strip оказался наиболее полезным. wasm-strip удаляет ненужные разделы и метаданные из модуля WebAssembly:

$ wasm-strip rotate_bg.wasm

Это уменьшает размер файла модуля ржавчины с 7,5 КБ до 6,6 КБ (после gzip).

васм-опт

wasm-opt — инструмент от Binaryen . Он берет модуль WebAssembly и пытается оптимизировать его как по размеру, так и по производительности, основываясь только на байт-коде. Некоторые инструменты, такие как Emscripten, уже используют этот инструмент, некоторые — нет. Обычно полезно попытаться сэкономить дополнительные байты с помощью этих инструментов.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

С помощью wasm-opt мы можем сбрить еще несколько байт, чтобы после gzip осталось 6,2 КБ.

#![no_std]

После некоторых консультаций и исследований мы переписали наш код Rust без использования стандартной библиотеки Rust, используя функцию #![no_std] . Это также полностью отключает динамическое распределение памяти, удаляя код распределителя из нашего модуля. Компиляция этого файла Rust с помощью

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

дал модуль Wasm размером 1,6 КБ после wasm-opt , wasm-strip и gzip. Хотя он все еще больше, чем модули, созданные C и AssemblyScript, он достаточно мал, чтобы считаться легким.

Производительность

Прежде чем мы сделаем поспешные выводы, основываясь только на размере файла: мы пошли на этот путь, чтобы оптимизировать производительность, а не размер файла. Итак, как мы измеряли производительность и каковы были результаты?

Как проводить бенчмаркинг

Несмотря на то, что WebAssembly является форматом байт-кода низкого уровня, его все равно необходимо отправить через компилятор для генерации машинного кода, специфичного для хоста. Как и в случае с JavaScript, компилятор работает в несколько этапов. Если говорить просто: первый этап компилируется намного быстрее, но имеет тенденцию генерировать более медленный код. Как только модуль запускается, браузер отслеживает, какие части часто используются, и отправляет их через более оптимизирующий, но более медленный компилятор.

Наш вариант использования интересен тем, что код для поворота изображения будет использоваться один, а может и два раза. Так что в подавляющем большинстве случаев мы никогда не получим преимуществ оптимизирующего компилятора. Это важно учитывать при проведении бенчмаркинга. Запуск наших модулей WebAssembly 10 000 раз в цикле дал бы нереалистичные результаты. Чтобы получить реалистичные цифры, нам следует запустить модуль один раз и принимать решения на основе цифр, полученных в результате этого единственного запуска.

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

Сравнение скорости по языкам
Сравнение скорости для каждого браузера

Эти два графика представляют собой разные взгляды на одни и те же данные. На первом графике мы сравниваем данные по браузерам, на втором графике — по используемым языкам. Обратите внимание, что я выбрал логарифмическую шкалу времени. Также важно, чтобы во всех тестах использовалось одно и то же тестовое изображение с разрешением 16 мегапикселей и один и тот же хост-компьютер, за исключением одного браузера, который нельзя было запустить на одном компьютере.

Не анализируя слишком много этих графиков, становится ясно, что мы решили нашу первоначальную проблему с производительностью: все модули WebAssembly выполняются примерно за 500 мс или меньше. Это подтверждает то, о чем мы говорили в начале: WebAssembly обеспечивает предсказуемую производительность. Независимо от того, какой язык мы выбираем, разница между браузерами и языками минимальна. Если быть точным: стандартное отклонение JavaScript во всех браузерах составляет ~ 400 мс, тогда как стандартное отклонение всех наших модулей WebAssembly во всех браузерах составляет ~ 80 мс.

Усилие

Еще одним показателем является количество усилий, которые нам пришлось приложить для создания и интеграции нашего модуля WebAssembly в Squoosh. Трудно присвоить усилиям числовое значение, поэтому я не буду создавать графики, но есть несколько вещей, на которые я хотел бы обратить внимание:

AssemblyScript работал без проблем. Он не только позволяет использовать TypeScript для написания WebAssembly, что упрощает проверку кода для моих коллег, но также позволяет создавать очень маленькие модули WebAssembly без клея, которые имеют очень маленькие размеры и приличную производительность. Инструменты в экосистеме TypeScript, такие как prettier и tslint, скорее всего, будут работать.

Rust в сочетании с wasm-pack также чрезвычайно удобен, но лучше подходит для более крупных проектов WebAssembly, где необходимы привязки и управление памятью. Нам пришлось немного отклониться от счастливого пути, чтобы добиться конкурентоспособного размера файла.

C и Emscripten создали очень маленький и высокопроизводительный модуль WebAssembly из коробки, но без смелости перейти к связующему коду и сократить его до самого необходимого, общий размер (модуль WebAssembly + связующий код) в конечном итоге оказывается довольно большим.

Заключение

Итак, какой язык вам следует использовать, если у вас есть горячий путь JS и вы хотите сделать его быстрее или более совместимым с WebAssembly. Как всегда в случае с вопросами о производительности, ответ таков: это зависит. Итак, что мы отправили?

Сравнительный график

Сравнивая соотношение размера модуля и производительности различных языков, которые мы использовали, лучшим выбором кажется C или AssemblyScript. Мы решили выпустить Rust . Для этого решения есть несколько причин: все кодеки, поставляемые в Squoosh, скомпилированы с использованием Emscripten. Мы хотели расширить наши знания об экосистеме WebAssembly и использовать другой язык в производстве . AssemblyScript — сильная альтернатива, но проект относительно молод, а компилятор не так зрел, как компилятор Rust.

Хотя разница в размере файла между Rust и другими языками выглядит довольно существенной на диаграмме разброса, на самом деле она не так уж и велика: загрузка 500 байт или 1,6 КБ даже через 2G занимает менее 1/10 секунды. И мы надеемся, что Rust скоро сократит разрыв в размерах модулей.

Что касается производительности во время выполнения, у Rust средний показатель в браузерах выше, чем у AssemblyScript. Особенно в крупных проектах Rust с большей вероятностью будет создавать более быстрый код без необходимости ручной оптимизации кода. Но это не должно мешать вам использовать то, что вам наиболее удобно.

При всем этом: AssemblyScript стал великим открытием. Это позволяет веб-разработчикам создавать модули WebAssembly без необходимости изучения нового языка. Команда AssemblyScript очень быстро отреагировала и активно работает над улучшением своего инструментария. Мы обязательно будем следить за AssemblyScript в будущем.

Обновление: ржавчина

После публикации этой статьи Ник Фицджеральд из команды Rust указал нам на их великолепную книгу Rust Wasm, в которой есть раздел об оптимизации размера файлов . Следование инструкциям (в частности, оптимизация времени компоновки и ручная обработка паники) позволило нам написать «нормальный» код Rust и вернуться к использованию Cargo ( npm Rust) без увеличения размера файла. Модуль Rust после gzip получает 370B. Для получения подробной информации, пожалуйста, ознакомьтесь с пиаром, который я открыл на Squoosh .

Особая благодарность Эшли Уильямс , Стиву Клабнику , Нику Фицджеральду и Максу Грею за всю их помощь в этом путешествии.

,

Это стабильно быстро, йоу.

В своих предыдущих статьях я рассказывал о том, как WebAssembly позволяет перенести библиотечную экосистему C/C++ в Интернет. Одним из приложений, широко использующих библиотеки C/C++, является squoosh , наше веб-приложение, которое позволяет сжимать изображения с помощью различных кодеков, скомпилированных из C++ в WebAssembly.

WebAssembly — это виртуальная машина низкого уровня, выполняющая байт-код, хранящийся в файлах .wasm . Этот байт-код строго типизирован и структурирован таким образом, что его можно скомпилировать и оптимизировать для хост-системы гораздо быстрее, чем это может сделать JavaScript. WebAssembly предоставляет среду для запуска кода, в котором с самого начала учитывалась изолированная программная среда и встраивание.

По моему опыту, большинство проблем с производительностью в Интернете вызвано принудительной компоновкой и чрезмерным рисованием, но время от времени приложению приходится выполнять вычислительно затратную задачу, которая отнимает много времени. WebAssembly может помочь здесь.

Горячий путь

В squoosh мы написали функцию JavaScript , которая поворачивает буфер изображения на угол, кратный 90 градусам. Хотя OffscreenCanvas был бы идеальным для этого, он не поддерживается в целевых браузерах и немного глючит в Chrome .

Эта функция перебирает каждый пиксель входного изображения и копирует его в другую позицию выходного изображения для достижения поворота. Для изображения размером 4094х4096 пикселей (16 мегапикселей) потребуется более 16 миллионов итераций внутреннего блока кода, что мы называем «горячим путем». Несмотря на такое довольно большое количество итераций, два из трёх протестированных нами браузеров выполняют задачу за 2 секунды или меньше. Приемлемая продолжительность для такого типа взаимодействия.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Однако один браузер занимает более 8 секунд. Способ, которым браузеры оптимизируют JavaScript, действительно сложен , и разные движки оптимизируют разные вещи. Некоторые оптимизируют необработанное выполнение, некоторые оптимизируют взаимодействие с DOM. В данном случае мы попали по неоптимизированному пути в одном браузере.

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

WebAssembly для предсказуемой производительности

В целом, JavaScript и WebAssembly могут достичь одинаковой максимальной производительности. Однако для JavaScript такой производительности можно достичь только «быстрым путем», и зачастую сложно оставаться на этом «быстром пути». Одним из ключевых преимуществ WebAssembly является предсказуемая производительность даже в разных браузерах. Строгая типизация и низкоуровневая архитектура позволяют компилятору обеспечивать более строгие гарантии, что код WebAssembly нужно оптимизировать только один раз и он всегда будет использовать «быстрый путь».

Написание для WebAssembly

Ранее мы взяли библиотеки C/C++ и скомпилировали их в WebAssembly, чтобы использовать их функциональность в Интернете. Мы особо не трогали код библиотек, а просто написали небольшие фрагменты кода на C/C++, чтобы сформировать мост между браузером и библиотекой. На этот раз наша мотивация другая: мы хотим написать что-то с нуля, имея в виду WebAssembly, чтобы можно было использовать преимущества WebAssembly.

Архитектура веб-сборки

При написании для WebAssembly полезно немного больше понять, что такое WebAssembly на самом деле.

Цитирую WebAssembly.org :

Когда вы компилируете фрагмент кода C или Rust в WebAssembly, вы получаете файл .wasm , содержащий объявление модуля. Это объявление состоит из списка «импорта», который модуль ожидает от своей среды, списка экспорта, который этот модуль делает доступным хосту (функции, константы, фрагменты памяти) и, конечно же, фактических двоичных инструкций для функций, содержащихся внутри. .

Кое-что я не осознавал, пока не изучил это: стек, который делает WebAssembly «виртуальной машиной на основе стека», не хранится в том фрагменте памяти, который используют модули WebAssembly. Стек полностью является внутренним и недоступен веб-разработчикам (кроме DevTools). Таким образом, можно писать модули WebAssembly, которым вообще не нужна дополнительная память и которые используют только внутренний стек виртуальной машины.

В нашем случае нам нужно будет использовать некоторую дополнительную память, чтобы обеспечить произвольный доступ к пикселям нашего изображения и создать повернутую версию этого изображения. Для этого и нужен WebAssembly.Memory .

Управление памятью

Обычно, как только вы используете дополнительную память, вам необходимо каким-то образом управлять этой памятью. Какие части памяти используются? Какие из них бесплатны? В C, например, есть функция malloc(n) , которая находит пространство памяти из n последовательных байтов. Функции такого типа еще называют «распределителями». Конечно, реализация используемого распределителя должна быть включена в ваш модуль WebAssembly и увеличит размер вашего файла. Размер и производительность этих функций управления памятью могут существенно различаться в зависимости от используемого алгоритма, поэтому многие языки предлагают на выбор несколько реализаций («dmalloc», «emmalloc», «wee_alloc» и т. д.).

В нашем случае мы знаем размеры входного изображения (и, следовательно, размеры выходного изображения) до запуска модуля WebAssembly. Здесь мы увидели возможность: традиционно мы передавали буфер RGBA входного изображения в качестве параметра функции WebAssembly и возвращали повернутое изображение в качестве возвращаемого значения. Чтобы сгенерировать это возвращаемое значение, нам придется использовать распределитель. Но поскольку мы знаем общий объем необходимой памяти (в два раза больше размера входного изображения, один раз для ввода и один раз для вывода), мы можем поместить входное изображение в память WebAssembly с помощью JavaScript , запустить модуль WebAssembly для генерации второго, повернутое изображение, а затем используйте JavaScript, чтобы прочитать результат. Мы можем обойтись вообще без использования какого-либо управления памятью!

Избалован выбором

Если вы посмотрите на исходную функцию JavaScript , которую мы хотим использовать в WebAssembly, то увидите, что это чисто вычислительный код без API-интерфейсов, специфичных для JavaScript. Таким образом, портировать этот код на любой язык должно быть довольно просто. Мы оценили три разных языка, которые компилируются в WebAssembly: C/C++, Rust и AssemblyScript. Единственный вопрос, на который нам нужно ответить для каждого из языков: как получить доступ к необработанной памяти без использования функций управления памятью?

С и Эмскриптен

Emscripten — это компилятор C для цели WebAssembly. Цель Emscripten — служить заменой широко известных компиляторов C, таких как GCC или clang, и в основном совместим с флагами. Это основная часть миссии Emscripten, поскольку компания хочет максимально упростить компиляцию существующего кода C и C++ в WebAssembly.

Доступ к необработанной памяти заложен в самой природе C, и указатели существуют именно по этой причине:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Здесь мы превращаем число 0x124 в указатель на беззнаковые 8-битные целые числа (или байты). Это фактически превращает переменную ptr в массив, начинающийся с адреса памяти 0x124 , который мы можем использовать как любой другой массив, позволяя нам получать доступ к отдельным байтам для чтения и записи. В нашем случае мы смотрим на буфер RGBA изображения, порядок которого мы хотим изменить для достижения вращения. Чтобы переместить пиксель, нам фактически нужно переместить одновременно 4 последовательных байта (по одному байту на каждый канал: R, G, B и A). Чтобы упростить задачу, мы можем создать массив 32-битных целых чисел без знака. По соглашению, наше входное изображение начинается с адреса 4, а выходное изображение начинается сразу после окончания входного изображения:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

После переноса всей функции JavaScript на C мы можем скомпилировать файл C с помощью emcc :

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Как всегда, emscripten генерирует файл связующего кода под названием c.js и модуль wasm под названием c.wasm . Обратите внимание, что модуль Wasm сжимается с помощью gzip всего до 260 байт, а код соединения после gzip занимает около 3,5 КБ. После некоторых усилий нам удалось отказаться от связующего кода и создать экземпляры модулей WebAssembly с помощью ванильных API. Это часто возможно с помощью Emscripten, если вы не используете ничего из стандартной библиотеки C.

Ржавчина

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

Одним из таких инструментов является wasm-pack , созданный рабочей группой Rustwasm . wasm-pack берет ваш код и превращает его в веб-модуль, который готово работать с такими сборщиками, как webpack. wasm-pack — чрезвычайно удобный инструмент, но на данный момент он работает только для Rust. Группа рассматривает возможность добавления поддержки других языков, ориентированных на WebAssembly.

В Rust срезы — это то же самое, что массивы в C. И, как и в C, нам нужно создавать срезы, которые используют наши начальные адреса. Это противоречит модели безопасности памяти, которую применяет Rust, поэтому, чтобы добиться своего, нам приходится использовать ключевое слово unsafe , позволяющее нам писать код, не соответствующий этой модели.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Компиляция файлов Rust с использованием

$ wasm-pack build

дает модуль Wasm размером 7,6 КБ с примерно 100 байтами связующего кода (оба после gzip).

Ассемблерскрипт

AssemblyScript — довольно молодой проект, целью которого является компилятор TypeScript в WebAssembly. Однако важно отметить, что он не будет просто использовать TypeScript. AssemblyScript использует тот же синтаксис, что и TypeScript, но заменяет стандартную библиотеку своей собственной. Их стандартная библиотека моделирует возможности Webassembly. Это означает, что вы не можете просто скомпилировать любую TypeScript, которую вы можете лежать в Webassembly, но это означает , что вам не нужно изучать новый язык программирования для написания Webassembly!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Учитывая поверхность небольшого типа, которую имеет наша функция rotate() , было довольно легко переносить этот код в сборку. Функции load<T>(ptr: usize) и store<T>(ptr: usize, value: T) предоставляются AssemblyScript для доступа к необработанной памяти. Чтобы скомпилировать наш файл AssemblyScript , нам нужно только установить пакет NPM AssemblyScript/assemblyscript и запустить

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

Assemblyscript предоставит нам модуль ~ 300 байт WASM и без кода клея. Модуль просто работает с API Vanilla Webassembly.

ПРИМЕНЕНИЯ WEBASSEMBLY

Rust's 7,6 КБ удивительно велик по сравнению с двумя другими языками. В экосистеме Webassembly есть пара инструментов, которые могут помочь вам проанализировать ваши файлы webassembly (независимо от языка, с которым создается) и рассказать вам, что происходит, а также поможет вам улучшить вашу ситуацию.

Твигги

Twiggy - еще один инструмент от команды Rust's Webassembly, который извлекает кучу проницательных данных из модуля Webassembly. Инструмент не является специфичным для ржавчины и позволяет проверять такие вещи, как график вызовов модуля, определять неиспользованные или лишнюю секции и выяснить, какие разделы способствуют общему размеру файла вашего модуля. Последнее может быть сделано с помощью top команды Twiggy:

$ twiggy top rotate_bg.wasm
Скриншот по установке Twiggy

В этом случае мы видим, что большая часть нашего размера файла проистекает из распределения. Это было удивительно, так как наш код не использует динамические распределения. Еще одним важным фактором является подраздел «имена функций».

Wasm-Strip

wasm-strip -это инструмент из бинарного инструментария Webassembly , или Wabt для краткости. Он содержит несколько инструментов, которые позволяют вам осматривать и манипулировать модулями веб -ассемемности. wasm2wat -это разборщик, который превращает бинарный модуль WASM в читаемый на человеке формат. Wabt также содержит wat2wasm , который позволяет вам превратить этот читаемый формат обратно в бинарный модуль WASM. В то время как мы использовали эти два дополнительных инструмента для проверки наших файлов webassembly, мы обнаружили, что wasm-strip наиболее полезными. wasm-strip удаляет ненужные разделы и метаданные из модуля Webassembly:

$ wasm-strip rotate_bg.wasm

Это уменьшает размер файла модуля ржавчины с 7,5 КБ до 6,6 КБ (после GZIP).

wasm-opt

wasm-opt -это инструмент от Binaryen . Он требует модуля Webassembly и пытается оптимизировать его как для размера, так и для производительности, основанных только на байт -коде. Некоторые инструменты, такие как Emscripten, уже запускают этот инструмент, некоторые нет. Обычно хорошая идея - попытаться сэкономить дополнительные байты, используя эти инструменты.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

С wasm-opt мы можем сбрить еще одну горстку байтов, чтобы оставить в общей сложности 6,2 КБ после GZIP.

#! [no_std]

После некоторых консультаций и исследований мы переписываем наш код Rust без использования стандартной библиотеки Rust, используя функцию #![no_std] . Это также полностью отключает динамическое распределение памяти, удаляя код распределения из нашего модуля. Скомпилировать этот файл ржавчины с

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

Получил модуль WASM 1,6 КБ после wasm-opt , wasm-strip и GZIP. Несмотря на то, что он все еще больше, чем модули, генерируемые C и Assemblyscript, он достаточно мал, чтобы считаться легким.

Производительность

Прежде чем сделать выводы, основанные только на размере файла - мы отправились в это путешествие, чтобы оптимизировать производительность, а не размер файла. Итак, как мы измеряли производительность и каковы были результаты?

Как сравнить

Несмотря на то, что Webassembly является форматом байт-кодов низкого уровня, его все равно необходимо отправлять через компилятор для генерации машинного кода, специфичного для хоста. Так же, как JavaScript, компилятор работает на нескольких этапах. Проще говоря: первый этап намного быстрее в компиляции, но имеет тенденцию генерировать более медленный код. Как только модуль начинает работать, браузер отмечает, какие детали часто используются, и отправляет их через более оптимизирующий, но более медленный компилятор.

Наш вариант использования интересен тем, что код для вращения изображения будет использоваться один раз, может быть, дважды. Таким образом, в подавляющем большинстве случаев мы никогда не получим преимущества оптимизирующего компилятора. Это важно иметь в виду при сравнительном анализе. Запуск наших модулей Webassembly 10 000 раз в цикле даст нереалистичные результаты. Чтобы получить реалистичные цифры, мы должны запустить модуль один раз и принимать решения на основе чисел из этого единого запуска.

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

Сравнение скорости на язык
Сравнение скорости в браузере

Эти два графика представляют собой разные представления на одни и те же данные. На первом графике мы сравниваем по браузере, во втором графике мы сравниваем по используемому языку. Обратите внимание, что я выбрал логарифмический шкалу времени. Также важно, чтобы все тесты использовали одно и то же 16 -мегапиксельное испытательное изображение и одну и ту же хост -машину, за исключением одного браузера, который нельзя было запустить на одной машине.

Не анализируя эти графики, ясно, что мы решили нашу первоначальную проблему производительности: все модули WebAssembly работают за ~ 500 мс или меньше. Это подтверждает то, что мы изложили в начале: Webassembly дает вам предсказуемую производительность. Независимо от того, какой язык мы выбираем, дисперсия между браузерами и языками минимальна. Точнее: стандартное отклонение JavaScript во всех браузерах составляет ~ 400 мс, в то время как стандартное отклонение всех наших модулей Webassembly во всех браузерах составляет ~ 80 мс.

Усилие

Другая метрика - это количество усилий, которые мы должны были приложить, чтобы создать и интегрировать наш модуль Webassembly в Squoosh. Трудно присвоить числовое значение усилиям, поэтому я не буду создавать никаких графиков, но есть несколько вещей, которые я хотел бы указать:

Сборка была без трения. Это не только позволяет вам использовать TypeScript для написания WebAssembly, что делает обзор кода очень простым для моих коллег, но также создает бесплатные модули веб-ассемембели, которые очень маленькие с достойной производительностью. Инструменты в экосистеме TypeScript, такой как красиво и TSLINT, скорее всего, просто работают.

Rust в сочетании с wasm-pack также чрезвычайно удобна, но преуспевает больше в больших проектах WebAssembly, которые были привязки, и необходимо управление памятью. Мы должны были немного расходиться с Happy Path, чтобы достичь конкурентного размера файла.

C и Emscripten создали очень маленький и высокопрофессиональный модуль Webassembly из коробки, но без смелости прыгнуть в код клея и уменьшить его до обнаженных потребностей.

Заключение

Итак, какой язык вы должны использовать, если у вас есть горячий путь JS и вы хотите сделать его быстрее или более соответствовать Webassembly. Как всегда с вопросами о производительности, ответ: это зависит. Так что мы отправили?

Сравнительный график

Сравнивая в компромиссном размере модуля / производительности различных языков, которые мы использовали, лучший выбор, по -видимому, является либо C, либо Assemblyscript. Мы решили отправить ржавчину . Есть несколько причин для этого решения: все кодеки, отправленные в Squoosh, составлены с использованием Emscripten. Мы хотели расширить наши знания об экосистеме Webassembly и использовать другой язык в производстве . Assemblyscript является сильной альтернативой, но проект относительно молод, а компилятор не такой зрелый, как компилятор ржавчины.

В то время как разница в размере файла между Rust и другим размером языков выглядит довольно радикальной на графике рассеяния, это не так уж важно в действительности: загрузка 500B или 1,6 КБ даже более 2 г занимает менее 1/10 секунды. И, мы надеемся, скоро закроет разрыв с точки зрения размера модуля.

С точки зрения производительности выполнения, Rust имеет более быстрое среднее значение по браузерам, чем AssemblyScript. Особенно на более крупных проектах Rust с большей вероятностью будет производить более быстрый код без необходимости ручной оптимизации кода. Но это не должно помешать вам использовать то, что вам больше всего удобно.

Все, что говорится: Assemblyscript была отличным открытием. Это позволяет веб -разработчикам производить модули Webassembly без необходимости изучать новый язык. Команда Assemblyscript была очень отзывчивой и активно работает над улучшением их инструментов. Мы обязательно будем следить за сборкой в ​​будущем.

Обновление: ржавчина

После публикации этой статьи Ник Фицджеральд из команды Rust указал нам на их превосходную книгу Rust wasm, которая содержит раздел об оптимизации размера файла . Следуя инструкциям (наиболее особенно включив оптимизацию времени ссылки и ручная обработка паники) позволило нам написать «нормальный» код ржавчины и вернуться к использованию Cargo ( npm из ржавчины), не раздувая размер файла. Модуль ржавчины заканчивается 370B после GZIP. Для получения подробной информации, пожалуйста, посмотрите на PR, который я открыл на Squoosh .

Особая благодарность Эшли Уильямс , Стиву Клабника , Нику Фицджеральду и Максу Грэи за всю их помощь в этом путешествии.

,

Это постоянно быстро, йо

В своих предыдущих статьях я говорил о том, как Webassembly позволяет вам принести библиотечную экосистему C/C ++ в Интернет. Одним из приложений, которое широко использует библиотеки C/C ++, является Squoosh , наше веб -приложение, которое позволяет вам сжимать изображения с различными кодеками, которые были составлены из C ++ в Webassembly.

Webassembly-это виртуальная машина низкого уровня, которая запускает байт-код, который хранится в файлах .wasm . Этот байтовый код сильно напечатан и структурирован таким образом, что он может быть скомпилирован и оптимизирован для системы хоста намного быстрее, чем JavaScript CAN. Webassembly предоставляет среду для запуска кода, в котором с самого начала имел в виду песочницу и внедряет.

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

Горячий путь

В Squoosh мы написали функцию JavaScript , которая поворачивает буфер изображения на несколько градусов. В то время как вне экрана был бы идеальным для этого, он не поддерживается в браузерах, на которые мы нацеливались, и немного багги в Chrome .

Эта функция отражается на каждом пикселе входного изображения и копирует его в другое положение в выходном изображении для достижения вращения. Для изображения 4094px на 4096px (16 мегапикселей) потребуется более 16 миллионов итераций внутреннего кодового блока, что мы называем «горячим путем». Несмотря на это довольно большое количество итераций, два из трех браузеров, которые мы протестировали, завершают задачу за 2 секунды или меньше. Приемлемая продолжительность для этого типа взаимодействия.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Один браузер, однако, занимает более 8 секунд. То, как браузеры оптимизируют JavaScript, действительно сложный , и различные двигатели оптимизируются для разных вещей. Некоторые оптимизируют для необработанного выполнения, некоторые оптимизируют для взаимодействия с DOM. В этом случае мы достигли неоптимизированного пути в одном браузере.

Webassembly, с другой стороны, полностью построена вокруг необработанной скорости выполнения. Так что, если мы хотим быстро, предсказуемой производительности в браузерах для такого кода, Webassembly может помочь.

Webassembly для предсказуемой производительности

В целом, JavaScript и Webassembly могут достичь такой же пиковой производительности. Тем не менее, для JavaScript эта производительность может быть достигнута только по «быстрому пути», и часто сложно оставаться на этом «быстром пути». Одним из ключевых преимуществ, которые предлагает Webassembly, является предсказуемая производительность даже в разных браузерах. Строгая архитектура на типинг и низкоуровневый позволяет компилятору делать более сильные гарантии, так что код веб-ассемильи должен быть оптимизирован только один раз и всегда будет использовать «быстрый путь».

Написание для Webassembly

Ранее мы принимали библиотеки C/C ++ и собрали их в Webassembly, чтобы использовать их функциональность в Интернете. Мы действительно не касались кода библиотек, мы только что написали небольшие объемы кода C/C ++, чтобы сформировать мост между браузером и библиотекой. На этот раз наша мотивация отличается: мы хотим написать что -то с нуля с учетом веб -ассимбильны, чтобы мы могли использовать преимущества, которые обладает Webassembly.

Архитектура Webassembly

При написании для Webassembly полезно понять немного больше о том, что на самом деле является Webassembly.

Цитировать webassembly.org :

Когда вы собираете кусок кода C или Rust в Webassembly, вы получаете файл .wasm , который содержит объявление модуля. Это объявление состоит из списка «импорта», которые модуль ожидает от своей среды, списка экспортов, которые этот модуль предоставляет для хоста (функции, константы, куски памяти) и, конечно, фактические бинарные инструкции для функций, содержащихся внутри .

То, что я не осознавал, пока не изучил это: стек, который делает Webassembly «виртуальной машиной на основе стека», не хранится в кусочке памяти, которую используют модули Webassembly. Стек является полностью виртуальной вечерней и недоступной для веб-разработчиков (за исключением Devtools). Таким образом, можно писать модули Webassembly, которые вообще не нуждаются в дополнительной памяти, и использовать только виртуальную машину.

В нашем случае нам нужно будет использовать некоторую дополнительную память, чтобы обеспечить произвольный доступ к пикселям нашего изображения и создать вращающуюся версию этого изображения. Это то, для чего нужна WebAssembly.Memory .

Управление памятью

Обычно, когда вы используете дополнительную память, вы найдете необходимость каким -то образом управлять этой памятью. Какие части памяти используются? Какие из них бесплатны? Например, в C у вас есть функция malloc(n) , которая находит пространство памяти n последовательных байтов. Функции такого рода также называются «распределителями». Конечно, реализация используемого распределения должна быть включена в ваш модуль Webassembly и увеличит размер вашего файла. Этот размер и производительность этих функций управления памятью могут значительно варьироваться в зависимости от используемого алгоритма, поэтому многие языки предлагают несколько реализаций на выбор («dmalloc», «emmalloc», «wee_alloc» и т. Д.).

В нашем случае мы знаем размеры входного изображения (и, следовательно, размеры выходного изображения), прежде чем запустить модуль Webassembly. Здесь мы видели возможность: традиционно мы передавали буфер RGBA входного изображения в качестве параметра функции WebAssembly и возвращаем вращанное изображение в качестве возвращаемого значения. Чтобы сгенерировать это возвращаемое значение, мы должны были бы использовать распределитель. Но поскольку мы знаем общее количество необходимого памяти (вдвое больше входного изображения, один раз для ввода и один раз для вывода), мы можем поместить входное изображение в память Webassembly, используя JavaScript , запустите модуль WebAssembly для создания 2 -го, Повернуто изображение, а затем используйте JavaScript, чтобы прочитать результат. Мы можем вообще уйти, не используя управление памятью вообще!

Избаловано для выбора

Если вы посмотрели на оригинальную функцию JavaScript , которую мы хотим для Webassembly-FY, вы можете увидеть, что это чисто вычислительный код без API, специфичных для JavaScript. Таким образом, он должен быть довольно простым, чтобы перенести этот код на любой язык. Мы оценили 3 разных языка, которые компилируются с Webassembly: C/C ++, Rust и AssemblyScript. Единственный вопрос, который нам нужно ответить для каждого из языков: как мы доступны доступа к необработанной памяти без использования функций управления памятью?

C и Emscripten

Emscripten является компилятором C для цели Webassembly. Цель Emscripten состоит в том, чтобы функционировать как замену замены для хорошо известных компиляторов C, таких как GCC или Clang, и в основном совместим с флагом. Это основная часть миссии Emscripten, поскольку она хочет сделать компиляцию существующего кода C и C ++ как можно проще.

Доступ к необработанной памяти находится в самом природе C, и по этой причине существует указатели:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Здесь мы превращаем число 0x124 в указатель на беспигнированные 8-битные целые числа (или байты). Это эффективно превращает переменную ptr в массив, начиная с адреса памяти 0x124 , который мы можем использовать, как и любой другой массив, позволяя нам получить доступ к отдельным байтам для чтения и письма. В нашем случае мы смотрим на буфер RGBA изображения, который мы хотим повторно заказать для достижения вращения. Чтобы переместить пиксель, нам действительно нужно переместить 4 последовательных байта одновременно (один байт для каждого канала: R, G, B и A). Чтобы упростить это, мы можем создать множество 32-разрядных целых чисел. По соглашению, наше входное изображение начнется с адреса 4, а наше выходное изображение начнется сразу после окончания входного изображения:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

После переноса всей функции JavaScript в C мы можем скомпилировать файл C с помощью emcc :

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Как всегда, Emscripten генерирует файл кода клея, называемый c.js , и модуль WASM под названием c.wasm . Обратите внимание, что модуль WASM GZIP до ~ 260 байтов, а код клея составляет около 3,5 КБ после GZIP. После некоторого скрипания мы смогли отказаться от кода клея и создать экземпляр модулей Webassembly с помощью API -интерфейсов ванили. Это часто возможно с Emscripten, если вы ничего не используете из стандартной библиотеки C.

Ржавчина

Rust-это новый современный язык программирования с системой богатых типов, без времени выполнения и модели владения, которая гарантирует безопасность памяти и безопасность потоков. Rust также поддерживает Webassembly в качестве основной функции, и команда Rust внесла много отличных инструментов в экосистему Webassembly.

Одним из этих инструментов является wasm-pack , от рабочей группы Rustwasm . wasm-pack берет ваш код и превращает его в модуль для веб-сайтов, который работает из бокса, таких как WebPack. wasm-pack -чрезвычайно удобный опыт, но в настоящее время работает только для Rust. Группа рассматривает возможность добавить поддержку другим языкам, направленным на целеустремленность.

В ржавчине ломтики - это то, что массивы находятся в C., и, как и в C, нам нужно создать ломтики, которые используют наши начальные адреса. Это противоречит модели безопасности памяти, которую применяет Руст, поэтому, чтобы получить свой путь, мы должны использовать unsafe ключевое слово, позволяя нам написать код, который не соответствует этой модели.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Скомпилирование файлов ржавчины с помощью

$ wasm-pack build

Дает модуль WASM 7,6 КБ с примерно 100 байтами клея (оба после GZIP).

Assemblyscript

Assemblyscript -это довольно молодой проект, который стремится стать компилятором TypeScript-To-Webassembly. Важно отметить, однако, что он не будет просто потреблять какую -либо типа. AssemblyScript использует тот же синтаксис, что и TypeScript, но выключает стандартную библиотеку для своей собственной. Их стандартная библиотека моделирует возможности Webassembly. Это означает, что вы не можете просто скомпилировать любую TypeScript, которую вы можете лежать в Webassembly, но это означает , что вам не нужно изучать новый язык программирования для написания Webassembly!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Учитывая поверхность небольшого типа, которую имеет наша функция rotate() , было довольно легко переносить этот код в сборку. Функции load<T>(ptr: usize) и store<T>(ptr: usize, value: T) предоставляются AssemblyScript для доступа к необработанной памяти. Чтобы скомпилировать наш файл AssemblyScript , нам нужно только установить пакет NPM AssemblyScript/assemblyscript и запустить

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

Assemblyscript предоставит нам модуль ~ 300 байт WASM и без кода клея. Модуль просто работает с API Vanilla Webassembly.

ПРИМЕНЕНИЯ WEBASSEMBLY

Rust's 7,6 КБ удивительно велик по сравнению с двумя другими языками. В экосистеме Webassembly есть пара инструментов, которые могут помочь вам проанализировать ваши файлы webassembly (независимо от языка, с которым создается) и рассказать вам, что происходит, а также поможет вам улучшить вашу ситуацию.

Твигги

Twiggy - еще один инструмент от команды Rust's Webassembly, который извлекает кучу проницательных данных из модуля Webassembly. Инструмент не является специфичным для ржавчины и позволяет проверять такие вещи, как график вызовов модуля, определять неиспользованные или лишнюю секции и выяснить, какие разделы способствуют общему размеру файла вашего модуля. Последнее может быть сделано с помощью top команды Twiggy:

$ twiggy top rotate_bg.wasm
Скриншот по установке Twiggy

В этом случае мы видим, что большая часть нашего размера файла проистекает из распределения. Это было удивительно, так как наш код не использует динамические распределения. Еще одним важным фактором является подраздел «имена функций».

Wasm-Strip

wasm-strip -это инструмент из бинарного инструментария Webassembly , или Wabt для краткости. Он содержит несколько инструментов, которые позволяют вам осматривать и манипулировать модулями веб -ассемемности. wasm2wat -это разборщик, который превращает бинарный модуль WASM в читаемый на человеке формат. Wabt также содержит wat2wasm , который позволяет вам превратить этот читаемый формат обратно в бинарный модуль WASM. В то время как мы использовали эти два дополнительных инструмента для проверки наших файлов webassembly, мы обнаружили, что wasm-strip наиболее полезными. wasm-strip удаляет ненужные разделы и метаданные из модуля Webassembly:

$ wasm-strip rotate_bg.wasm

Это уменьшает размер файла модуля ржавчины с 7,5 КБ до 6,6 КБ (после GZIP).

wasm-opt

wasm-opt -это инструмент от Binaryen . Он требует модуля Webassembly и пытается оптимизировать его как для размера, так и для производительности, основанных только на байт -коде. Некоторые инструменты, такие как Emscripten, уже запускают этот инструмент, некоторые нет. Обычно хорошая идея - попытаться сэкономить дополнительные байты, используя эти инструменты.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

С wasm-opt мы можем сбрить еще одну горстку байтов, чтобы оставить в общей сложности 6,2 КБ после GZIP.

#! [no_std]

После некоторых консультаций и исследований мы переписываем наш код Rust без использования стандартной библиотеки Rust, используя функцию #![no_std] . Это также полностью отключает динамическое распределение памяти, удаляя код распределения из нашего модуля. Скомпилировать этот файл ржавчины с

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

Получил модуль WASM 1,6 КБ после wasm-opt , wasm-strip и GZIP. Несмотря на то, что он все еще больше, чем модули, генерируемые C и Assemblyscript, он достаточно мал, чтобы считаться легким.

Производительность

Прежде чем сделать выводы, основанные только на размере файла - мы отправились в это путешествие, чтобы оптимизировать производительность, а не размер файла. Итак, как мы измеряли производительность и каковы были результаты?

Как сравнить

Несмотря на то, что Webassembly является форматом байт-кодов низкого уровня, его все равно необходимо отправлять через компилятор для генерации машинного кода, специфичного для хоста. Так же, как JavaScript, компилятор работает на нескольких этапах. Проще говоря: первый этап намного быстрее в компиляции, но имеет тенденцию генерировать более медленный код. Как только модуль начинает работать, браузер отмечает, какие детали часто используются, и отправляет их через более оптимизирующий, но более медленный компилятор.

Наш вариант использования интересен тем, что код для вращения изображения будет использоваться один раз, может быть, дважды. Таким образом, в подавляющем большинстве случаев мы никогда не получим преимущества оптимизирующего компилятора. Это важно иметь в виду при сравнительном анализе. Запуск наших модулей Webassembly 10 000 раз в цикле даст нереалистичные результаты. Чтобы получить реалистичные цифры, мы должны запустить модуль один раз и принимать решения на основе чисел из этого единого запуска.

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

Сравнение скорости на язык
Сравнение скорости в браузере

Эти два графика представляют собой разные представления на одни и те же данные. На первом графике мы сравниваем по браузере, во втором графике мы сравниваем по используемому языку. Обратите внимание, что я выбрал логарифмический шкалу времени. Также важно, чтобы все тесты использовали одно и то же 16 -мегапиксельное испытательное изображение и одну и ту же хост -машину, за исключением одного браузера, который нельзя было запустить на одной машине.

Не анализируя эти графики, ясно, что мы решили нашу первоначальную проблему производительности: все модули WebAssembly работают за ~ 500 мс или меньше. Это подтверждает то, что мы изложили в начале: Webassembly дает вам предсказуемую производительность. Независимо от того, какой язык мы выбираем, дисперсия между браузерами и языками минимальна. Точнее: стандартное отклонение JavaScript во всех браузерах составляет ~ 400 мс, в то время как стандартное отклонение всех наших модулей Webassembly во всех браузерах составляет ~ 80 мс.

Усилие

Другая метрика - это количество усилий, которые мы должны были приложить, чтобы создать и интегрировать наш модуль Webassembly в Squoosh. Трудно присвоить числовое значение усилиям, поэтому я не буду создавать никаких графиков, но есть несколько вещей, которые я хотел бы указать:

Сборка была без трения. Это не только позволяет вам использовать TypeScript для написания WebAssembly, что делает обзор кода очень простым для моих коллег, но также создает бесплатные модули веб-ассемембели, которые очень маленькие с достойной производительностью. Инструменты в экосистеме TypeScript, такой как красиво и TSLINT, скорее всего, просто работают.

Rust в сочетании с wasm-pack также чрезвычайно удобна, но преуспевает больше в больших проектах WebAssembly, которые были привязки, и необходимо управление памятью. Мы должны были немного расходиться с Happy Path, чтобы достичь конкурентного размера файла.

C и Emscripten создали очень маленький и высокопрофессиональный модуль Webassembly из коробки, но без смелости прыгнуть в код клея и уменьшить его до обнаженных потребностей.

Заключение

Итак, какой язык вы должны использовать, если у вас есть горячий путь JS и вы хотите сделать его быстрее или более соответствовать Webassembly. Как всегда с вопросами о производительности, ответ: это зависит. Так что мы отправили?

Сравнительный график

Сравнивая в компромиссном размере модуля / производительности различных языков, которые мы использовали, лучший выбор, по -видимому, является либо C, либо Assemblyscript. Мы решили отправить ржавчину . Есть несколько причин для этого решения: все кодеки, отправленные в Squoosh, составлены с использованием Emscripten. Мы хотели расширить наши знания об экосистеме Webassembly и использовать другой язык в производстве . Assemblyscript является сильной альтернативой, но проект относительно молод, а компилятор не такой зрелый, как компилятор ржавчины.

В то время как разница в размере файла между Rust и другим размером языков выглядит довольно радикальной на графике рассеяния, это не так уж важно в действительности: загрузка 500B или 1,6 КБ даже более 2 г занимает менее 1/10 секунды. И, мы надеемся, скоро закроет разрыв с точки зрения размера модуля.

С точки зрения производительности выполнения, Rust имеет более быстрое среднее значение по браузерам, чем AssemblyScript. Особенно на более крупных проектах Rust с большей вероятностью будет производить более быстрый код без необходимости ручной оптимизации кода. Но это не должно помешать вам использовать то, что вам больше всего удобно.

Все, что говорится: Assemblyscript была отличным открытием. Это позволяет веб -разработчикам производить модули Webassembly без необходимости изучать новый язык. Команда Assemblyscript была очень отзывчивой и активно работает над улучшением их инструментов. Мы обязательно будем следить за сборкой в ​​будущем.

Обновление: ржавчина

После публикации этой статьи Ник Фицджеральд из команды Rust указал нам на их превосходную книгу Rust wasm, которая содержит раздел об оптимизации размера файла . Следуя инструкциям (наиболее особенно включив оптимизацию времени ссылки и ручная обработка паники) позволило нам написать «нормальный» код ржавчины и вернуться к использованию Cargo ( npm из ржавчины), не раздувая размер файла. Модуль ржавчины заканчивается 370B после GZIP. Для получения подробной информации, пожалуйста, посмотрите на PR, который я открыл на Squoosh .

Особая благодарность Эшли Уильямс , Стиву Клабника , Нику Фицджеральду и Максу Грэи за всю их помощь в этом путешествии.