Главная » Хабрахабр » [Перевод] Как мы удвоили скорость работы с Float в Mono

[Перевод] Как мы удвоили скорость работы с Float в Mono

Мой друг Aras недавно написал один и тот же трассировщик лучей на разных языках, в том числе на C++, C# и компиляторе Unity Burst. Разумеется, естественно ожидать, что C# будет медленнее, чем C++, но мне показалось интересным, что Mono настолько медленнее .NET Core.

Опубликованные им показатели были плохими:

  • C# (.NET Core): Mac 17.5 Mray/s,
  • C# (Unity, Mono): Mac 4.6 Mray/s,
  • C# (Unity, IL2CPP): Mac 17.1 Mray/s

Я решил посмотреть, что происходит, и задокументировать места, которые можно улучшить.

В результате этого бенчмарка и изучения этой проблемы мы обнаружили три области, в которых возможно улучшение:

  • Во-первых, необходимо улучшить параметры Mono по умолчанию, потому что пользователи обычно не настраивают параметры у себя
  • Во-вторых, нам нужно активнее знакомить мир с бекэндом оптимизации кода LLVM в Mono
  • В-третьих, мы улучшили настройку некоторых параметров Mono.

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

NET Core были следующими: Результаты на моём домашнем iMac для Mono и .

Рабочая среда

Результаты, MRay/sec

.NET Core 2.1.4, отладочная сборка dotnet run

3.6

.NET Core 2.1.4, релизная сборка dotnet run -c Release

21.7

Ванильный Mono, mono Maths.exe

6.6

Ванильный Mono с LLVM и float32

15.5

В процессе исследования этой задачи мы обнаружили пару проблем, после исправления которых получены следующие результаты:

Рабочая среда

Результаты, MRay/sec

Mono с LLVM и float32

15.5

Усовершенствованный Mono с LLVM, float32 и fixed inline

29.6

Общая картина:

Просто применив LLVM и float32, можно почти в 2,3 раза увеличить производительность кода с плавающей запятой. А после настройки, которую мы добавили к Mono в результате этих опытов, можно повысить производительность в 4,4 раза по сравнению со стандартным Mono — эти параметры в будущих версиях Mono станут параметрами по умолчанию.

В этой статье я объясню наши находки.

32-битные и 64-битные Float

Aras использует для основной части вычислений 32-битные числа с плавающей запятой (тип float в C# или System.Single в .NET). В Mono мы давным давно совершили ошибку — все 32-битные вычисления с плавающей запятой выполнялись как 64-битные, а данные всё равно хранились в 32-битных областях.

Сегодня моя память не так остра, как раньше, и я не могу точно вспомнить, почему мы приняли такое решение.

Могу только предположить, что на него повлияли тенденции и идеи того времени.

Например, в процессорах Intel x87 для вычислений с плавающей запятой использовалась 80-битная точность, даже когда операнды были double, что обеспечивало пользователям более точные результаты. Тогда вокруг float-вычислений с повышенной точностью витала положительная аура.

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

В C99, Posix и ISO были добавлены 32-битные версии, но в те дни они не были широко доступны для всей отрасли (например, sinf — это float-версия sin, fabsf — версия fabs, и так далее). На начальных этапах развития Mono большинство математических операций, выполняемых на всех платформах, могло получать на входе только double.

Если говорить вкратце, то начало 2000-х было временем оптимизма.

Она становилась заметной только в некоторых научных бенчмарках, а 2003 году они редко разрабатывались на . Приложения платили большую цену за увеличение времени вычислений, но Mono в основном использовался для десктопных приложений Linux, обслуживающих HTTP-страницы и некоторые серверные процессы, поэтому скорость вычислений с плавающей точкой не была той проблемой, с которой мы сталкивались ежедневно. NET.

Беда не приходит одна, и здесь нет исключений. Сегодня игры, трёхмерные приложения, обработка изображений, VR, AR и машинное обучение сделали операции с плавающей запятой более распространённым типом данных. Они превратились в лавину, от которой никуда не спрятаться. Float больше не были дружелюбным типом данных, которые использовались в коде всего в паре мест. Их стало очень много и их распространение нельзя остановить.

Флаг float32 рабочей среды

Поэтому пару лет назад мы решили добавить поддержку выполнения 32-битных float-операций с помощью 32-битных операций, как и во всех других случаях. Мы назвали эту функцию рабочей среды «float32». В Mono она включается добавлением в рабочей среде опции --O=float32, а в приложениях Xamarin этот параметр изменяется в настройках проекта.

Мы рекомендовали мобильным пользователям одновременно включить оптимизирующий компилятор LLVM и флаг float32. Этот новый флаг хорошо восприняли наши мобильные пользователи, потому что в основном мобильные устройства до сих пор не слишком мощны, и им предпочтительнее обрабатывать данные быстрее, чем иметь повышенную точность.

Однако мы начали сталкиваться со случаям, в которых сюрпризы возникают из-за стандартного 64-битного поведения, см. Хоть этот флаг и реализован уже несколько лет, мы не делали его применяемым по умолчанию, чтобы избежать неприятных сюрпризов для пользователей. этот bug report, отправленный пользователем Unity.

Теперь мы по умолчанию будем использовать в Mono float32, прогресс можно отслеживать здесь: https://github.com/mono/mono/issues/6985.

Он использовал новые API, которые были добавлены в . Тем временем я вернулся к проекту своего друга Aras. Хотя . NET Core. Math всё равно выполняет преобразования из float в double. NET Core всегда выполнял 32-битные float-операции как 32-битные float, в процессе своей работы API System. Sin (double), при этом придётся выполнить преобразование из float в double. Например, если вам нужно вычислить функцию синуса для float-значения, то единственным вариантом является вызов Math.

NET Core был добавлен новый тип System. Чтобы это исправить, в . MathF] в Mono. MathF, в котором содержатся математические операции с плавающей запятой одинарной точности, и сейчас мы только что перенесли этот [System.

Переход от 64-битных к 32-битным float заметно повышает производительность, что можно увидеть из данной таблицы:

Рабочая среда и опции

Mrays/second

Mono с System.Math

6.6

Mono с System.Math и -O=float32

8.1

Mono с System.MathF

6.5

Mono с System.MathF и -O=float32

8.2

То есть применение float32 в этом тесте действительно улучшает производительность, а MathF оказывает незначительное влияние.
В процессе этого исследования мы обнаружили, что хотя в компиляторе Fast JIT Mono есть поддержка float32, мы не добавили эту поддержку в бекэнд LLVM. Это означало, что Mono с LLVM по-прежнему выполнял затратные преобразования из float в double.

Поэтому Золтан добавил в движок генерации кода LLVM поддержку float32.

При работе с Fast JIT необходимо соблюдать баланс между скоростью JIT и скоростью выполнения, поэтому мы ограничили количество встраиваемого кода, чтобы снизить объём работы движка JIT. Потом он заметил, что наш встраиватель кода (inliner) использует для Fast JIT те же эвристики, которые использовались для LLVM.

Сегодня этот параметр можно изменять с помощью переменной окружения MONO_INLINELIMIT, но на самом деле его нужно записать в значения по умолчанию. Но если ты решаешь использовать в Mono LLVM, то стремишься к как можно более быстрому коду, поэтому мы соответствующим образом изменили настройки.

Вот как выглядят результаты с изменёнными настройками LLVM:

Рабочая среда и опции

Mrays/seconds

Mono с System.Math --llvm -O=float32

16.0

Mono с System.Math --llvm -O=float32, постоянные эвристики

29.1

Mono с System.MathF --llvm -O=float32, постоянные эвристики

29.6

Для внесения всех этих усовершенствований достаточно было незначительных усилий. К этим изменениям привели периодические обсуждения в Slack. Мне даже удалось выкроить несколько часов одним вечером, чтобы перенести System.MathF в Mono.

Мы хотим найти другое подобное ПО, которое можно использовать для изучения генерируемого нами двоичного кода, и убедиться, что мы передаём LLVM наилучшие данные для оптимального выполнения его работы. Код трассировщика лучей Aras стал идеальным объектом для изучения, потому что он был самодостаточным, являлся реальным приложением, а не синтетическим бенчмарком.

Также мы подумываем об обновлении используемого нами LLVM, и использовании новых добавленных оптимизаций.

Дополнительная точность имеет приятные побочные эффекты. Например, читая пул-реквесты движка Godot, я увидел, что там ведётся активное обсуждение того, делать ли точность операций с плавающей запятой настраиваемой во время компиляции (https://github.com/godotengine/godot/pull/17134).

Я спросил Хуана, зачем это может кому-то понадобиться, ведь я считал, что играм вполне достаточно 32-битных операций с плавающей запятой.

Можно использовать разные стратегии, чтобы снизить влияние этой проблемы, и одна из них — работа с повышенной точностью, за которую приходится расплачиваться производительностью. Хуан объяснил, что в общем случае float работают замечательно, но если вы «отойдёте» от центра, допустим, переместитесь на 100 километров от центра игры, то начинает накапливаться ошибка вычислений, что в результате может привести к интересным графическим глитчам.

Вскоре после нашего разговора в своей ленте Twitter я увидел пост, демонстрирующий эту проблему: http://pharr.org/matt/blog/2018/03/02/rendering-in-camera-space.html

Здесь мы видим модель спортивного автомобиля из пакета pbrt-v3-scenes **. Проблема показана на изображениях ниже. И камера, и сцена находятся рядом с точкой начала координат, и всё выглядит отлично.

** (Автор модели автомобиля Yasutoshi Mori.)

Видно, что модель машины стала довольно фрагментарной; это происходит исключительно из-за нехватки точности чисел с плавающей запятой. Потом мы перемещаем камеру и сцену на 200 000 единиц по xx, yy и zz от точки начала координат.

Если мы переместимся ещё дальше в 5×5×5 раз, на 1 миллион единиц от точки начала координат, то модель начинает распадаться; машина превращается в чрезвычайно грубую воксельную аппроксимацию самой себя, одновременно интересную и ужасающую. (Keanu задал вопрос: Minecraft такой кубический просто потому, что всё рендерится очень далеко от начала координат?)

** (Приношу извинения Yasutoshi Mori за то, что мы сделали с его красивой моделью.)


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

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

*

x

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

Вырастить и научить. Как мы подружились с PEGA

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

В ЕС добиваются права на ремонт крупной бытовой техники

Источник: Schraube locker!? Им удалось собрать более 100 тыс. Европейские активисты движения «права на ремонт» решили начать с холодильников. подписей под соответствующим документом и даже добиться голосования в Брюсселе по поводу изменения законодательства, имеющего отношение к ремонту бытовой техники. Традиционная ...