Некоторое время назад много усилий было потрачено на то, чтобы убедить программистов заменить приближения "пи" (3.14159…) на приближения "тау" (6.28318…). Идея, судя по многочисленным постам в блогах и видео на YouTube, заключалась в том, что распространённые формулы становятся проще, а работать с константой, описывающей целую окружность, удобнее, чем с константой для половины окружности.
В целом с этим можно согласиться. Пусть это и небольшой момент, но он стоит внимания: код действительно становится немного лучше, если заменить пи на тау.
Однако за всей этой шумихой была упущена гораздо более значимая возможность. Вместо того чтобы заменять пи на тау, в большинстве случаев пи можно полностью убрать.
Как это работает
Сначала стоит рассмотреть типичный случай использования пи и тау в коде: преобразование значений в радианы и обратно для вызова тригонометрических функций. Если приходилось пользоваться этими константами, то, скорее всего, большая часть написанного кода выглядела примерно так:
y = center.y + (center.y * Math::sin(h * Math_TAU) * s) -
(cursor->get_height() / 2);
Это не выдуманный пример — это фрагмент, найденный случайным поиском по слову "tau" в исходном коде движка Godot Engine на GitHub. Именно такой фрагмент, и десятки похожих на него, находятся при подобном поиске.
В Godot нет ничего особенного в этом плане. Открыв практически любой другой игровой движок, можно выполнить точно такой же поиск и увидеть точно такое же использование.
Обратите внимание, что здесь происходит: программист имеет значение h, которое уже периодично на диапазоне от 0 до 1, но умножает его на тау, потому что нужно вызвать sin.
На первый взгляд это выглядит разумно, если не заглядывать дальше. Но что происходит внутри реализации sin?
Существует множество реализаций sin, но независимо от того, какую из них взять, почти в самом начале функции обнаружится что-то подобное:
_PS256_CONST(cephes_FOPI, 1.27323954473516);
...
y = _mm256_mul_ps(x, *(v8sf*)_ps256_cephes_FOPI);
Снова не выдуманный пример — это фрагмент из широко используемой реализации sin на AVX2. Ничего необычного или странного в этом нет — практически любая быстрая тригонометрическая библиотека делает что-то очень похожее.
Что делает эта строка? Она умножает входное значение на константу 1.27323954473516.
А эта константа — не что иное, как 4/пи.
Таким образом, вызывающий код делает следующее:
sin(h * 2 * pi)
а код библиотеки сразу же делает вот это:
y = (4 / pi) * x
то есть вызывающий код умножает на коэффициент пи только для того, чтобы код библиотеки тут же поделил его обратно. Это буквально преобразование в радианы и обратно без всякой причины. Если бы оба программиста просто договорились не использовать радианы, а вместо этого работали в исходном диапазоне [0, 1], в котором h уже находилось, задача упростилась бы для обоих: вызывающая сторона экономит одно умножение, а библиотека получает более понятную и точную константу.
И момент с "точностью" на самом деле весьма интересен. Мало того что за бессмысленное преобразование в радианы приходится платить лишним умножением — стоит также отметить, что все распространённые радианные углы, кроме 0, сложно представить точно. Хочется сохранить 90 градусов в радианах? Сколько бит ни используй, точного значения не получится никогда.
А вот 90 градусов на диапазоне [0, 1] — это просто 0.25, битовый паттерн, для которого вообще не требуется ни одного бита мантиссы! 0.5? То же самое! 0.75? Достаточно одного бита мантиссы, чтобы представить значение точно.
Так что диапазон [0, 1] не только более эффективен вычислительно по сравнению с радианами, но и более компактен и точен при представлении типичных значений, часто встречающихся на практике.
Математика не требует радиан
Можно понять, почему у некоторых такой переход вызывает беспокойство. Даже если поверить, что пользовательский код всегда умножает на пи или тау, а библиотечный код всегда делит обратно, всё равно может остаться то самое "чувство урока математики" — ощущение, будто отказ от радиан это что-то неправильное.
Но математика никогда не постановляла, что синус и косинус обязаны принимать аргументы в радианах!
Идея параметризовать окружность от нуля до единицы вместо от нуля до тау — не случайная выдумка для этого блога. Это вполне легитимная, давно существующая математическая конструкция, у которой даже есть собственное название — оборот (turn).
В оборотах 0 соответствует 0 градусам, 0.5 — 180 градусам, 1 — 360 градусам, 2 — 720 градусам и так далее. Это именно то, что требовалось.
Так что если есть опасение, что учитель математики будет недоволен, поводов для беспокойства нет. Достаточно объяснить, что вопрос был тщательно продуман, и параметризация углов в оборотах вместо радиан оказалась наиболее эффективным методом для конкретной задачи!
Переход прост
Если математическая библиотека написана самостоятельно или скопирована из чужого проекта, скорее всего, уже понятно, как отказаться от радиан и убрать пи и тау из кодовой базы. Достаточно взять функции sin и cos и сделать так, чтобы они принимали обороты вместо радиан — обычно для этого нужна лишь небольшая правка одной константы.
Если требуется поддержка старого кода, для новых тригонометрических функций на оборотах стоит выбрать другое имя. Тогда старый код по-прежнему сможет пользоваться прежними радианными sin и cos, реализовав их как тонкие обёртки над новыми функциями с делением на тау внутри.
Это очень просто — переход занимает всего несколько строк кода.
Впрочем, хотя обороты и представляются наиболее удобной репараметризацией, это не единственный вариант. Особенно если своя тригонометрия не пишется с нуля (а порой и в этом случае тоже), стоит рассмотреть вариант с полуоборотами, где полная окружность соответствует диапазону [0, 2]. Это немного менее интуитивно, но…
Такой подход уже есть в некоторых библиотеках
Оказывается (и это забавное совпадение!), что при желании в некоторых математических библиотеках уже можно найти функции sin и cos, параметризованные полуоборотами, а не радианами. Например, встроенная функция sincospi в CUDA вычисляет синус и косинус входного значения, умноженного на пи, то есть работает именно с полуоборотами.
Это отличная новость. Если целевая платформа уже поддерживает sincospi, можно прямо сейчас перестать использовать константы пи и тау в коде, вообще не трогая библиотеки. Достаточно начать вызывать sincospi с полуоборотами вместо sin и cos с радианами — и всё готово.
Чем меньше тау и пи, тем лучше
Опыт работы с целыми кодовыми базами, где радианы были полностью исключены, показывает, что скучать по ним не приходится. Все лишние тау и пи исчезают, а код читается яснее.
Та же логика изменения sin и cos применима и к остальным стандартным тригонометрическим функциям, так что при желании радианы можно убрать вообще везде. Библиотеки почти всегда внутри всё равно преобразуют значения из радиан, а на выходе конвертируют обратно в радианы, поэтому переход на обороты или полуобороты повсеместно обычно сводится просто к удалению кода — и почти ничего больше.