Многие реализации библиотеки идут глубоко в FPATAN instuction для всех дуговых функций. Как внедрен FPATAN? Предполагая, что у нас есть 1 битовый знак, M бит mantissa и N бит exponent, каков алгоритм, чтобы получить арктангенс этого числа? Там должен быть такой алгоритм, поскольку FPU делает это.
Как осуществляется арктан?
Ответ 1
Тригонометрические функции имеют довольно уродливые реализации, которые являются хакерскими и занимают много места. Я думаю, что будет довольно сложно найти кого-то, кто сможет объяснить алгоритм, который на самом деле используется.
Вот реализация atan2: https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/ieee754/dbl-64/e_atan2.c;h=a287ca6656b210c77367eec3c46d72f18476d61d;hb=HEAD
Изменение: На самом деле я нашел это: http://www.netlib.org/fdlibm/e_atan2.c, за которым намного легче следовать, но, вероятно, медленнее из-за этого (?).
FPU делает все это в некоторых цепях, поэтому CPU не должен выполнять всю эту работу.
Ответ 2
Реализации инструкций FPATAN на процессорах x86 обычно являются собственностью. Для вычисления arctan или других (обратных) тригонометрических функций общие алгоритмы следуют трехэтапному процессу:
- уменьшение аргумента для отображения полного входного домена в узкий интервал
- вычисление аппроксимации ядра на узком интервале (интервал первичной аппроксимации)
- расширение промежуточного результата на основе уменьшения аргумента для получения конечного результата
Сокращение аргументов обычно основано на хорошо известных тригонометрических тождествах, которые можно искать в различных стандартных ссылках, таких как MathWorld (http://mathworld.wolfram.com/InverseTangent.html). Для вычисления arctan обычно используются тождества
- arctan (-x) = -arctan (x)
- arctan (1/x) = 0,5 * pi-arctan (x) [x > 0]
- arctan (x) = arctan (c) + arctan ((x - c)/(1 + x * c))
Заметим, что последнее тождество поддается построению таблицы значений arctan (i/2 n), я = 1... 2 n что позволяет использовать произвольно узкий первичный интервал аппроксимации за счет дополнительного хранения таблиц. Это классический программный компромисс между пространством и временем.
Аппроксимация на интервале ядер обычно является минимаксным полиномиальным приближением достаточной степени. Рациональные аппроксимации обычно не конкурентоспособны на современном оборудовании из-за высокой стоимости деления с плавающей запятой, а также страдают от дополнительной числовой ошибки из-за вычисления двух многочленов плюс ошибка, вызванная делением.
Коэффициенты для минимаксных полиномиальных аппроксимаций обычно вычисляются с использованием алгоритма Ремеза (http://en.wikipedia.org/wiki/Remez_algorithm). Такие инструменты, как Maple и Mathematica, имеют встроенные средства для вычисления таких приближений. Точность полиномиальных аппроксимаций можно улучшить, убедившись, что все коэффициенты являются точно представимыми машинными числами. Единственный инструмент, о котором я знаю, имеет встроенное средство для этого - Sollya (http://sollya.gforge.inria.fr/), который предлагает функцию fpminimax()
.
Оценка полиномов обычно использует схему Хорнера (http://en.wikipedia.org/wiki/Horner%27s_method), которая является эффективной и точной, или смесь схемы Эстрина (http://en.wikipedia.org/wiki/Estrin%27s_scheme) и Horner's. Схема Estrin позволяет отлично использовать уровень команд parallelism, обеспечиваемый суперскалярными процессорами, незначительно влияя на общий подсчет команд и часто (но не всегда) на доброкачественное воздействие на точность.
Использование FMA (добавление с добавлением плавного умножения) повышает точность и производительность любой схемы оценки из-за уменьшения числа шагов округления и предоставления некоторой защиты от субтрактивного сокращения. FMA можно найти на многих процессорах, включая графические процессоры и последние процессоры x86. В стандарте C и стандартном С++ операция FMA отображается как стандартная библиотечная функция fma()
, однако ее необходимо эмулировать на платформах, которые не предлагают аппаратную поддержку, что замедляет работу на этих платформах.
С точки зрения программирования хотелось бы избежать риска ошибок преобразования при переводе констант с плавающей запятой, необходимых для сокращения аппроксимации и аргументации от текстового представления к машине. Процедура преобразования ASCII-to-floating-point известна тем, что содержит сложные ошибки (например, http://www.exploringbinary.com/php-hangs-on-numeric-value-2-2250738585072011e-308/). Один механизм, предлагаемый стандартным C ( не С++, который я знаю, где он доступен только как проприетарное расширение), должен указывать константы с плавающей запятой в виде шестнадцатеричных литералов, которые непосредственно выражают базовый бит-шаблон, эффективно избегая сложных преобразований.
Ниже приведен код C для вычисления double-precision arctan(), который демонстрирует многие из принципов и технологий проектирования, упомянутых выше. Этот быстро построенный код не обладает сложностью реализаций, указанных в других ответах, но должен предоставлять результаты с ошибками менее 2 ulps, которые могут быть достаточными в различных контекстах. Я создал пользовательскую минимаксную аппроксимацию с простой реализацией алгоритма Ремеза, который использовал 1024-битную арифметику с плавающей запятой для всех промежуточных этапов. Я ожидаю, что использование Sollya или подобных инструментов приведет к численно превосходящим приближениям.
double my_atan (double x)
{
double a, z, p, r, s, q, o;
/* argument reduction:
arctan (-x) = -arctan(x);
arctan (1/x) = 1/2 * pi - arctan (x), when x > 0
*/
z = fabs (x);
a = (z > 1.0) ? 1.0 / z : z;
/* evaluate minimax polynomial approximation */
s = a * a; // a**2
q = s * s; // a**4
o = q * q; // a**8
/* use Estrin scheme for low-order terms */
p = fma (fma (fma (-0x1.53e1d2a25ff34p-16, s, 0x1.d3b63dbb65af4p-13), q,
fma (-0x1.312788dde0801p-10, s, 0x1.f9690c82492dbp-9)), o,
fma (fma (-0x1.2cf5aabc7cef3p-7, s, 0x1.162b0b2a3bfcep-6), q,
fma (-0x1.a7256feb6fc5cp-6, s, 0x1.171560ce4a483p-5)));
/* use Horner scheme for high-order terms */
p = fma (fma (fma (fma (fma (fma (fma (fma (fma (fma (fma (fma (p, s,
-0x1.4f44d841450e1p-5), s,
0x1.7ee3d3f36bb94p-5), s,
-0x1.ad32ae04a9fd1p-5), s,
0x1.e17813d66954fp-5), s,
-0x1.11089ca9a5bcdp-4), s,
0x1.3b12b2db51738p-4), s,
-0x1.745d022f8dc5cp-4), s,
0x1.c71c709dfe927p-4), s,
-0x1.2492491fa1744p-3), s,
0x1.99999999840d2p-3), s,
-0x1.555555555544cp-2) * s, a, a);
/* back substitution based on argument reduction */
r = (z > 1.0) ? (0x1.921fb54442d18p+0 - p) : p;
return copysign (r, x);
}
Ответ 3
Резюме: Это сложно. Кроме того, Eric Postpischil и Stephen Canon, которые иногда висят вокруг SO, очень хороши в этом.
Обычный подход для многих специальных функций выглядит следующим образом:
- Обрабатывать NaNs, бесконечности и подписанные нули в качестве особых случаев.
- Если число настолько велико, что результат округляется до
M_PI
, вернитеM_PI
. Вызовите этот порогM
. - Если есть какой-либо идентификатор сокращения аргументов, используйте его, чтобы привести аргумент в более удобный диапазон. ( Это может быть сложным: для
sin
иcos
это означает, что вы выбрали кратное точное значение 2pi, чтобы вы приземлились в правильном диапазоне.) - Разбить
[0,M)
на конечное число интервалов. Используйте Чебышевское приближение до arctan достаточно высокого порядка на каждом интервале. (Это делается в автономном режиме, и обычно это источник всех магических чисел, которые вы видите в этих реализациях. Кроме того, можно слегка приблизить приближение Чебышева, используя алгоритм обмена Remez, но мне неизвестны случаи, когда это помогает.) - Определите интервал, в котором находится аргумент (с использованием
if
и т.д. или просто трюк с индексированием таблицы), и оцените серию Чебышева на этом интервале.
Здесь особенно желательно несколько свойств:
- Реализация
arctan
должна быть монотонной; то есть, еслиx < y
, тоarctan(x) <= arctan(y)
. - Реализация
arctan
должна всегда возвращать ответ в пределах 1 ulp правильного ответа. Обратите внимание, что это относительная ошибка.
Нецелесообразно оценивать ряды Чебышева, чтобы эти два свойства выполнялись. Здесь часто встречаются трюки, в которых два double
используются для представления разных частей одного значения. Тогда, вероятно, есть некоторые проблемы, чтобы показать, что реализация монотонна. Кроме того, около нуля, приближение Тейлора к arctan
вместо приближения Чебышева --- вы после относительной ошибки и оцениваете серию с использованием правила Хорнера, должны работать.
Если вы ищете реализацию atan
для чтения, fdlibm кажется менее неприятным, чем тот, который сейчас находится в glibc. Сокращение аргументов, по-видимому, основано на триггерной идентичности tan(a+b) = (tan(a) + tan(b)) / (1 - tan(a) tan(b))
, используя 0.5
, 1
или 1.5
для tan(a)
, если это необходимо.