Главная » Хабрахабр » Опыт с WebAssembly или как С++ undefined behavior выстрелил в ногу

Опыт с WebAssembly или как С++ undefined behavior выстрелил в ногу

Под катом же будет текстовая версия всего относительно UB. На прошедшем C++ Russia 2018 мы рассказывали о нашем опыте перехода на WebAssembly, как наткнулись на UB и как его героически закостыляли, немного о самой технологии и как работает на разных устройствах. Код используемых тестов доступен на GitHub.

Схема проекта

Сам он написан на C++ с поддержкой Lua. client code
Бизнес логика пишется на C++ с использованием нашего framework. Так как есть код который зависит от платформы, то приходится использовать JavaScript и DOM.

Например, для просмотра видео, на десктопных платформах используется видеоплеер с кодеками от Google, а в браузере используется тег video

28
Логично предположить: произойдет переполнение типа int и будет выведено -2 147 483 648.

При этом все работает на asm.js и десктопных приложениях. 29
Нет, приложение упадет.

В документации по этому поводу сказано следующее: 30
Точка остановки программы указывает на код trunc_s или trunc_u.

Усечение float в int может вызвать исключение в том случае, если исходное значение float не помещается в диапазон integer или если это NaN.

Надо отловить все проблемные места в которых происходит преобразование float и double в int или контролировать это как то иначе.

Сейчас предлагается использовать опцию BINARYEN_TRAP_MODE со значением clamp. 31
Эта проблема уже много раз обсуждалась, и сейчас в разработке на эту тему находится новая спецификация, для стандарта WebAssembly под названием nontrapping-float-to-int-conversions. Если это невозможно, то усекает по максимальному или минимальному значению. Для всех случаев преобразования float в int будет использоваться обертка, которая проверяет возможность преобразования.

Был сделан простой тест, который наглядно показывает ситуацию с режимом clamp. 32
С опцией clamp приложение работало стабильно. Производительность падает примерно на 40% в сравнении со сборкой без опции clamp, а для версии 1. Один проход выполняет сто миллионов преобразований float и double в int. 17, что была доступна на тот момент, эта цифра была еще больше, так что на слайде это уже оптимизированный вариант режима clamp, но все же разрыв заметен. 37. Здесь цифры измерялись на процессоре Core i7 2,2 Ghz в Chrome версии 65 под Mac OS X.

режим clamp

Сейчас clamp по производительности практически не отличим от asm.js. Режим clamp был оптимизирован Alon Zakai он же kripken — разработчик emscripten, как раз на основе этого теста. Скачок в 619 миллисекунд, для asm.js это как раз первая итерация теста, для которой выполнялась JIT оптимизация.

Разрыв в случае wasm примерно в 20 раз. 33
Тот же тест на мобильных устройствах Samsung Edge S8+ и iPhone X. Это демонстрация того, как можно сделать JIT-компиляцию действительно оптимальной и производительной.

Под Android такие результаты относятся не только к данному устройству. На iPhone X разница между наличием и отсутствием clamp режима и составляет примерно 40%. На других устройствах результаты похожи или еще хуже.

Полгода назад эти цифры были совсем другими, если учесть, что тогда на iOS еще не было поддержки WebAssembly и сравнивать можно было только с практически неработающей asm.js версией.

Самый простой вариант это сравнить значение во float с границей numeric_limits<int>, а также сделать проверку не является ли значение NaN. 34
Было решено отловить проблемные места в коде с усечением float и double в integer.

Оптимизатор посчитал, что он может так сделать, так как не считает truncate опасной операцией. 35
Ключевое место, проверка на isfinite, было сгенерировано после усечения. В данном случае оптимизация -О3, но это не важно, так как любая оптимизация приведет к генерации такого кода.

На текущий момент эта проблема в компиляторе сохраняется, она известна, и ее решение находится в разработке, это nontrapping-float-to-int-conversions. Мы получаем неработающий код, даже если учли все кейсы.

Например, с опцией -О0 все проверки значения в нашей функции будут в том же порядке, что и написаны, но это влияет на производительность и размер выходного файла. 36
Поскольку на данный момент truncate в WebAssembly не безопасен, необходимо больше контроля для кодогенерации.

Если посмотреть на результаты синтетического теста, то разница в производительности при использовании функции и без нее сильно заметна и составляет примерно 70%. Можно ограничить оптимизацию только для этой функции указав для нее атрибут optnone. Результат с опцией clamp будет более производительнее чем с нашей функцией. В данном случае, выполняется одна операция: double преобразуется в int 20 миллионов раз. Тест.

Тот же самый тест. 37
На мобильных устройствах постоянное использование функции проверки значения также снижает производительность. Это достаточно критичные результаты для использования функции. Разрыв хорошо заметен на Android, более 80% и 70% на iOS. Соответственно использовать ее лучше только в тех случаях, где это действительно необходимо.

Эти значения могут проверятся еще во float или они как то изначально ограничены входящими данными. 38
Мы знаем, что в большинстве преобразований float в integer в нашей программе не могут выйти за определенные границы. То использование режима clamp будет избыточным, достаточно проверять только ту часть, где есть риск выхода за границы типа integer

Результат тестирования, это усечения девяти значений и одно из них проверяется функцией. В следующем тесте происходит перемножения матриц и проецирование точек на плоскость. Но разница не так хорошо видна на десктопе. Как видно, такой вариант уже дает прирост производительности примерно на 15% в сравнении с clamp.

В сравнении с clamp, Android и iOS показывают чуть больше 20% прироста. 39
На мобильных устройствах результат более заметен.

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

Emscripten позволяет работать с файлами из C++ так же, как и с обычными файлами. 40
Еще одна проблема с которой мы столкнулись, это чтение данных из файлов. В процессе работы нашего приложения часто создаются новые элементы, которые по необходимости подгружаются из файлов. Для этого создается file package, а затем он монтируется в JavaScript. В какой-то момент данные из этих файлов становились невалидными.

Размещение в heap повышает скорость доступа к файлам, но при реалокации памяти(если указана опция ALLOW_MEMORY_GROWTH), указатель станет не действительным, поэтому все что дальше мы считываем из файлов- это какой то мусор. 42
По умолчанию, после загрузки data-файла, данные копируются в heap, а затем используется указатель на эту область.

37. На тот момент(версия v1. Решение: не использовать heap для файловой системы. 17), это уже была известная проблема. На данный момент, в текущей версии тулчейна, флаг no-heap-copy будет указан автоматически если выставлена опция ALLOW_MEMORY_GROWTH. В таком случае будет напрямую использоваться объект из xhr response, то есть рантайм получает просто указатель на уже имеющийся буфер не копируя его в heap.

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

Но обычно используется 1-2 графика и на старте приложения такой объем памяти не используется. В нашем случае, все элементы приложения создаются динамически по мере необходимости, к примеру, пользователь может одновременно открыть до 9 графиков, которые в реальном времени получают котировки.

Мобильный браузер при компиляции выбросит исключение с out of memory. Потребовав больше памяти на старте, вы рискуете не запуститься на мобильных устройствах.

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

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

Придется писать на JavaScript поскольку других возможностей для взаимодействия с системными API — нет, или использовать готовые врапперы.

Это может очень сильно затруднить разработку. Отлаживать wasm в браузере очень сложно, если у вас нет версии под desktop. Некоторые обновления тулчейна вносят изменения, ломающие приложение.

WebAssembly стал поддерживаться в Safari, однако последующие обновления ломали и чинили WebAssembly. Непонятна ситуация с iOS. Поэтому пока используем его как основную версию на iPhone. Спасает то, что на текущий момент в Safari сильно оптимизировали выполнение asm.js и он стал запускаться значительно быстрее.

Главный плюс для нас, это ощутимый прирост в производительности на мобильных и "слабых" устройствах. 63
Технология вполне жива и реализована на достаточном уровне для ее использования в готовом проекте. На данный момент мы используем WebAssembly с сентября 2017 года.

Отдельно хочется поблагодарить Сергея Платонова sermp, за организацию конференции.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Как я строил гексапод в Space Engineers. Часть 1

Вектор: class Vector ; distanceTo(vector) { return Math.sqrt(Math.pow(this.x - vector.x, 2) + Math.pow(this.y - vector.y, 2) + Math.pow(this.z - vector.z, 2)); } diff(vector) { return new Vector( this.x - vector.x, this.y - vector.y, this.z - vector.z ); } add(vector) { ...

[Перевод] Принципы SOLID, о которых должен знать каждый разработчик

Объектно-ориентированное программирование принесло в разработку ПО новые подходы к проектированию приложений. В частности, ООП позволило программистам комбинировать сущности, объединённые некоей общей целью или функционалом, в отдельных классах, рассчитанных на решение самостоятельных задач и независимых от других частей приложения. Однако само ...