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

в 11:01, , рубрики: Asm.js, c++, wasm, web, Программирование

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

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

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

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

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

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

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

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

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

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

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

режим clamp

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Автор: RPG18

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js