Comments 22
Спасибо за отличное сравнение! Есть один вопрос-предложение: вроде как -Oz
оптимизирует размер, а не производительность (как -O3
). Может быть, если собрать с -O3
, то Emscripten побыстрее будет?
Не, Oz нужен чтобы финальный wasm бинарник не был слишком монструозным и его загрузка в памяти была минимальной. До wasm - размер js. O3 не даст сильного прироста к производительности в случае wasm. Разве что если только вручную через clang компилировать в таргет wasm-unknown-unknown.
Только O3 - это максимальная производительность и минимальный размер. Oz бесполезен полностью - просаживать производительность (минус 20% !! документация не врала) ради 5кб при том что zip архивирует в разы лучше вообще неразумно говорить об Oz.
O3 ради производительности может использовать агрессивный инлайнинг и анролл циклов. Как итог ваш бинарник может заметно вырасти на O3. Oz же позволяет избегать этого. Но да из-за необходимости непрямых вызовов оно в среднем медленее O3, при этом может наэкономить те же 20+% в размере. Отдельная история с квирками оптимизаций на O3 - можете погуглить, почему Linux использует только O2. Ну или в блоге PVS почитать кейсы про исчезновение memset из релиза.
при том что zip архивирует в разы лучше
сильно зависит от кейса использования модулей. одним вебом мир не оканчивается. Да и пруфов подозреваю все равно не дадите.
В моем случае между O3 и Oz - даже нет темы для обсуждения. Производительность +20% стоит лишнего незначительного размера, который после после архивации перестает быть недостатком. И код после оптимизации не ломается.
> сильно зависит от кейса использования модулей.
WASM бинари сами по себе прекрасно сжимается, для пруфа достаточно самостоятельно сжать в zip на скоростном профиле и увидеть, какой реальный размер файла будет передан по сети.
WASM бинари сами по себе прекрасно сжимается, для пруфа достаточно самостоятельно сжать в zip на скоростном профиле
ну, попробовал пожать демку меняя только уровни оптимизации:
.rwxr-xr-x 12k user 2 сен 19:30 triangle-O2.wasm
.rwxr-xr-x 5,9k user 2 сен 19:30 triangle-O2.wasm.gz
.rwxr-xr-x 12k user 2 сен 19:30 triangle-O3.wasm
.rwxr-xr-x 5,9k user 2 сен 19:30 triangle-O3.wasm.gz
.rwxr-xr-x 11k user 2 сен 19:30 triangle-Oz.wasm
.rwxr-xr-x 5,5k user 2 сен 19:30 triangle-Oz.wasm.gz
10% разницы на равках и порядка 8-9 пожатые. Разницы в перформансе не замечено - зачем грузить больше если можно не грузить?
Пример, конечно же игрушечный, но вполне показательный.
Если перформанс точно не страдает - я не против, мои же тесты подтверждают мнение из документации,
https://emscripten.org/docs/optimizing/Optimizing-Code.html#trading-off-code-size-and-performance
Размер в ущерб производительности. В моем случае Oz дает просадку в 20%.
Давайте возьмем реальный проект.
https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd/ffmpeg-core.wasm
30mb васм в зипе уменьшается практически в три раза. Он и так то значительно тормознее чем нативный, совершенно нет разницы даже в том, что он весит на 5mb больше, главное, чтобы работал быстро
ну, то есть проблема у emscripten, а не у уровней оптимизации. Уже довольно продолжительное время существуют таргеты для компиляторов без участия ems. Примеры выше в том числе компилировались при помощи них через clang напрямую.
Ну а ffmpeg wasm это скорее POC, нежели реальный проект. Да и вопросов о том как он собирался тоже много. SIMD wasm, например, участвовал?
Не знаю на счет проблемы emscripten, он все же больше занимается оберточным делом и дает хорошие практики и со стороны js и при работе с памятью. Симды у ffmpeg есть только -msimd128, другие оптимизации никто руками не переписывал.
Не знаю на счет проблемы emscripten
разные компияторы генерируют бинарники разных размеров. Сам emscripten форкал llvm для поддержки компиляции в asm.js/wasm. Там же частично переимплементировал часть libc. Переносить изменения в форке на более новые версии компилятора сложно, поэтому высок шанс, что emscipten просто не поддерживает нативные wasm таргеты и как результат часть продвинутых оптимизаций становится недоступной. Поэтому Os/Oz у него не слишком продвинутые и заметно медленее O3. Да и размер wasm-бинаря в среднем был больше, чем при нативной компиляции. Некоторые пишут собственные компиляторы/транспиляторы и даже языки(moonbit, assembly script из статьи), чтобы генерировать как можно более маленький и производительный wasm. Меньше инструкций на вызов функции - выше скорость.
Не так давно в wasm завезли поддержку SIMD, которую в теории ffmpeg мог бы утилизировать. Делает ли он это - именно то о чём я спрашивал. И с большой долей вероятности ответ "не использует".
разные компияторы генерируют бинарники разных размеров. Сам emscripten форкал llvm для поддержки компиляции в asm.js/wasm.
Это давно было, asm.js уже забыт. с 19 года emscripten на
upstream LLVM WebAssembly backend. Он поддерживает нативные таргеты через upstream LLVM.
Там же частично переимплементировал часть libc.
А как иначе, это же для совместимости.Разве от него можно отказаться?
Кстати, зреет модульная замена libc EMCC_CFLAGS=-lllvmlibc
Сарказм:
Позволю трактовать график быстродействия так:
Автор знает rust примерно в 5 раз лучше, чем assemblyscript или C++. Уж больно большой отрыв, и 2-3 места практически одинаковы.
И спасибо за сравнение, интересная тема.
По отсутствию идиоматического кода и переизобретение уже существующих API я бы сказал, что автор знает Rust также плохо как и C++. Просто так сложилось, что многие оптимизации у Rust из коробки, включая возможность SIMD. Код на плюсах более неравномерно выделяет память, в отличие от Rust - есть Vec::with_capacity
в rust, но vector::reserve
я так и не нашёл, а capacity
явно указан не везде одинаково.
А можете привести примеры этого самого идиоматического кода на C++ или на Rust, который должен был написать человек, хорошо знакомый со спецификой языков и знающий "уже существующие API"?
Спасибо.
Если разбирать местный код:
minimum, maximum и absolute не нужны. есть std::cmp::{min, max} и f32::abs, которые по мнению компилятора могут быть заинлайнены.
pub fn new(in_x: f32, in_y: f32) -> Point {
Point{ x: in_x, y: in_y}
}
Превращается в
pub fn new(x: f32, y: f32) -> Self {
Point{x,y}
}
То бишь одноимённые параметры встают без присвоений. Аналогичная ситуация с TriangleCircle и Triangle. На производительность не влияет, но влияет на читабельность.
let mut indices: Vec<usize> = Vec::with_capacity(points_count);
for i in 0..points_count {
indices.push(i);
}
Превращается в
let mut indices = (0..points_count).collect();
Такой код генерирует меньше байткода при этом все ограничения на память присутствуют. Как минимум должно быть не медленнее исходного. Как максимум уменьшит memory footprint и как результат немного ускорится.
in_points.to_vec();
Традиционно копирование делается посредством `clone`.
Циклы в триангуляции по идее можно свести к map/filter/fold/reduce и потенциально получить дополнительное ускорение. Плюс кажется местами удаление из open_list в цикле делает что-то бесполезное.
for i in 0..(triangle_indices.len() / 3)
Ещё один напрашивающийся map
let mut triangles = triangle_indices
.chunks_exact(3)
.map(|chunk| Triangles::new(chunk.iter().map(|p| points[p].clone()).collect()))
.collect::Vec<_>()
В целом стоит почаще сводить задачи как map/filter/reduce, т.к. это помогает компилятору с проверкой и оптимизацией кода и в среднем обработка получает cache friendly для процессора. Плюс такие конструкции легко распараллеливаются каким-нибудь rayon, правда не для случая wasm.
Для тех кто стремится к идиоматичности можно посоветовать использовать cargo clippy, который подскажет как писать чутка идиоматичнее. Ну и читать доки:
Rust By Example - множество типовых примеров паттернов, которые используются в Rust
Rust Cookbook - примеры решения некоторых типовых проблем, встречающихся в программирование - логирование, параллелизм, работа с БД, CLI и пр.
P.S. Пожалуй стоит сделать дисклеймер - Rust позволяет писать код совершенно по разному, однако не делает никаких предположений касательно производительности и практически не может подсказывать как и когда использовать все встроенные в него механизмы, вроде того же chunk_exact. Поэтому не считайте этот комментарий кирпичом в сторону автора.
Касательно AssemblyScript - предполагаю, что можно увеличить быстродействия обернув все обращения к массивам в unchecked
, который отключает достаточно тяжёлые проверки на выход за границы массива.
Около двух лет назад переписывал часть приложения на AssemblyScript. Для меня его киллер-фича - это возможность получить на выходе обычный JavaScript если скомпилировать его с помощью TypeScript - очень сильно помогает при отладке. Но язык был ужасно сырой, только в процессе переписывания я создал более десятка багов в их репозитории, которые, к слову, были оперативно исправлены. Однако даже после оборачивание всего в unchecked
и частичного отключения встроенного GC код работал примерно с той же скоростью, что и оригинальный JavaScript, при этом всё ещё наблюдались отличия в поведении из-за багов, которые я уже не стал исследовать. Посмотрел сейчас в их репозиторий - и как будто за два года ничего принципиально не изменилось, куча достаточно мелких релизов с незначительными исправлениями.
А насколько эффективнее это будет работать в рантайме самой Node.js? Если есть преимущества, то можно будет менее болезненно писать хорошие библиотеки.
Зависит от того, что вы планируете писать в библиотеке. Если изобретать реакт на wasm с виртуальным DOM и прочим хтмлом, то скорее всего не сильно лучше. Если делать всякие тяжелые вычисления, как в статье - то получите прирост. Собственно для этих целей и делался wasm. У хрома и у Node.js один движок - V8 и весь JS код фактически JITился в wasm всё это время. Компилируя из языков с ручным управлением памяти вы получаете ровно эти самые бенефиты управления памятью и прирост в сравнении с аналогичным кодом на голом JS. Сами wasm модули распространяются как те же npm пакеты с JS-обёрткой. В случае с wasm-pack оно даже сгенерирует весь этот JS и подготовит для публикации.
Создание модуля WebAssembly с помощью Emscripten, AssemblyScript и Rust