All streams
Search
Write a publication
Pull to refresh

Comments 22

Не, 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

что-то вроде ржавого no_std чтобы библиотеку в wasm потоньше собирать?

типа того, не только под васм но и под железки

Сарказм:

Позволю трактовать график быстродействия так:

Автор знает rust примерно в 5 раз лучше, чем assemblyscript или C++. Уж больно большой отрыв, и 2-3 места практически одинаковы.

И спасибо за сравнение, интересная тема.

По отсутствию идиоматического кода и переизобретение уже существующих API я бы сказал, что автор знает Rust также плохо как и C++. Просто так сложилось, что многие оптимизации у Rust из коробки, включая возможность SIMD. Код на плюсах более неравномерно выделяет память, в отличие от Rust - есть Vec::with_capacity в rust, но vector::reserve я так и не нашёл, а capacity явно указан не везде одинаково.

А можете привести примеры этого самого идиоматического кода на C++ или на Rust, который должен был написать человек, хорошо знакомый со спецификой языков и знающий "уже существующие API"?
Спасибо.

Если разбирать местный код:

  1. 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 и подготовит для публикации.

Sign up to leave a comment.

Articles