С++ (и математика): быстрая аппроксимация тригонометрической функции
Я знаю, что это повторяющийся вопрос, но я пока не нашел полезного ответа. Я в основном ищу быстрое приближение функции acos
в С++, я хотел бы знать, могу ли я значительно побить стандартный.
Но некоторые из вас могут иметь представление о моей конкретной проблеме: я пишу научную программу, которая должна быть очень быстрой. Сложность основного алгоритма сводится к вычислению следующего выражения (много раз с разными параметрами):
sin( acos(t_1) + acos(t_2) + ... + acos(t_n) )
где t_i
- известные реальные (двойные) числа, а n
очень мала (например, меньше 6). Мне нужна точность не менее 1е-10. В настоящее время я использую стандартные функции sin
и acos
С++.
Как вы думаете, я могу как-то значительно увеличить скорость? Для тех из вас, кто знает некоторые математики, вы думаете, что было бы разумно расширить этот синус, чтобы получить алгебраическое выражение в терминах t_i
(включая только квадратные корни)?
Спасибо за ваши ответы.
Ответы
Ответ 1
Как упоминает Джонас Велицкий в комментариях, вы не можете сделать много компромиссов в точности.
Лучше всего попытаться использовать встроенные функции процессора для функций (если ваш компилятор не делает этого уже) и используя некоторую математику, чтобы уменьшить количество необходимых вычислений.
Также очень важно сохранить все в формате, удобном для процессоров, убедиться, что есть несколько промахов в кеше и т.д.
Если вы вычисляете большое количество функций, таких как acos
, возможно, переход на GPU является для вас вариантом?
Ответ 2
В приведенном ниже коде представлены простые реализации sin()
и acos()
, которые должны соответствовать вашим требованиям к точности и которые вы можете попробовать. Обратите внимание, что реализация математической библиотеки на вашей платформе очень сильно настраивается для конкретных аппаратных возможностей этой платформы и, вероятно, также кодируется в сборке для максимальной эффективности, поэтому простой скомпилированный код C, не поддерживающий специфику аппаратного обеспечения, вряд ли обеспечит более высокая производительность, даже если требования к точности несколько смягчены от полной двойной точности. Как отмечает Виктор Латыпов, также может оказаться целесообразным искать алгоритмические альтернативы, которые не требуют дорогостоящих вызовов трансцендентных математических функций.
В приведенном ниже коде я попытался придерживаться простых, переносимых конструкций. Если ваш компилятор поддерживает функцию rint()
[указанную C99 и С++ 11], вы можете использовать ее вместо my_rint()
. На некоторых платформах вызов floor()
может быть дорогостоящим, так как требует динамического изменения состояния машины. Функции my_rint()
, sin_core()
, cos_core()
и asin_core()
хотели бы быть встроены для лучшей производительности. Ваш компилятор может сделать это автоматически на высоких уровнях оптимизации (например, при компиляции с помощью -O3
), или вы можете добавить соответствующий атрибут вложения для этих функций, например. inline или __inline в зависимости от вашей инструментальной цепочки.
Не зная ничего о вашей платформе, я выбрал простые полиномиальные аппроксимации, которые оцениваются по схеме Эстрина плюс схема Хорнера. См. Википедию для описания этих схем оценки:
http://en.wikipedia.org/wiki/Estrin%27s_scheme,
http://en.wikipedia.org/wiki/Horner_scheme
Сами аппроксимации имеют минимаксный тип и были созданы для этого ответа с помощью алгоритма Ремеза:
http://en.wikipedia.org/wiki/Minimax_approximation_algorithm,
http://en.wikipedia.org/wiki/Remez_algorithm
Тождества, используемые в сокращении аргументов для acos()
, отмечены в комментариях, для sin()
я использовал сокращение аргументов стиля Коди/Уэйта, как описано в следующей книге:
W. J. Cody, W. Waite, Руководство по программному обеспечению для элементарных функций. Prentice-Hall, 1980
Оценки ошибок, упомянутые в комментариях, являются приблизительными и не были тщательно проверены или проверены.
/* not quite rint(), i.e. results not properly rounded to nearest-or-even */
double my_rint (double x)
{
double t = floor (fabs(x) + 0.5);
return (x < 0.0) ? -t : t;
}
/* minimax approximation to cos on [-pi/4, pi/4] with rel. err. ~= 7.5e-13 */
double cos_core (double x)
{
double x8, x4, x2;
x2 = x * x;
x4 = x2 * x2;
x8 = x4 * x4;
/* evaluate polynomial using Estrin scheme */
return (-2.7236370439787708e-7 * x2 + 2.4799852696610628e-5) * x8 +
(-1.3888885054799695e-3 * x2 + 4.1666666636943683e-2) * x4 +
(-4.9999999999963024e-1 * x2 + 1.0000000000000000e+0);
}
/* minimax approximation to sin on [-pi/4, pi/4] with rel. err. ~= 5.5e-12 */
double sin_core (double x)
{
double x4, x2, t;
x2 = x * x;
x4 = x2 * x2;
/* evaluate polynomial using a mix of Estrin and Horner scheme */
return ((2.7181216275479732e-6 * x2 - 1.9839312269456257e-4) * x4 +
(8.3333293048425631e-3 * x2 - 1.6666666640797048e-1)) * x2 * x + x;
}
/* minimax approximation to arcsin on [0, 0.5625] with rel. err. ~= 1.5e-11 */
double asin_core (double x)
{
double x8, x4, x2;
x2 = x * x;
x4 = x2 * x2;
x8 = x4 * x4;
/* evaluate polynomial using a mix of Estrin and Horner scheme */
return (((4.5334220547132049e-2 * x2 - 1.1226216762576600e-2) * x4 +
(2.6334281471361822e-2 * x2 + 2.0596336163223834e-2)) * x8 +
(3.0582043602875735e-2 * x2 + 4.4630538556294605e-2) * x4 +
(7.5000364034134126e-2 * x2 + 1.6666666300567365e-1)) * x2 * x + x;
}
/* relative error < 7e-12 on [-50000, 50000] */
double my_sin (double x)
{
double q, t;
int quadrant;
/* Cody-Waite style argument reduction */
q = my_rint (x * 6.3661977236758138e-1);
quadrant = (int)q;
t = x - q * 1.5707963267923333e+00;
t = t - q * 2.5633441515945189e-12;
if (quadrant & 1) {
t = cos_core(t);
} else {
t = sin_core(t);
}
return (quadrant & 2) ? -t : t;
}
/* relative error < 2e-11 on [-1, 1] */
double my_acos (double x)
{
double xa, t;
xa = fabs (x);
/* arcsin(x) = pi/2 - 2 * arcsin (sqrt ((1-x) / 2))
* arccos(x) = pi/2 - arcsin(x)
* arccos(x) = 2 * arcsin (sqrt ((1-x) / 2))
*/
if (xa > 0.5625) {
t = 2.0 * asin_core (sqrt (0.5 * (1.0 - xa)));
} else {
t = 1.5707963267948966 - asin_core (xa);
}
/* arccos (-x) = pi - arccos(x) */
return (x < 0.0) ? (3.1415926535897932 - t) : t;
}
Ответ 3
sin( acos(t1) + acos(t2) + ... + acos(tn) )
сводится к вычислению
sin( acos(x) ) and cos(acos(x))=x
потому что
sin(a+b) = cos(a)sin(b)+sin(a)cos(b).
Во-первых,
sin( acos(x) ) = sqrt(1-x*x)
Разложение в ряд Тейлора для sqrt сводит проблему к полиномиальным вычислениям.
Чтобы уточнить, здесь разложение на n = 2, n = 3:
sin( acos(t1) + acos(t2) ) = sin(acos(t1))cos(acos(t2)) + sin(acos(t2))cos(acos(t1) = sqrt(1-t1*t1) * t2 + sqrt(1-t2*t2) * t1
cos( acos(t2) + acos(t3) ) = cos(acos(t2)) cos(acos(t3)) - sin(acos(t2))sin(acos(t3)) = t2*t3 - sqrt(1-t2*t2)*sqrt(1-t3*t3)
sin( acos(t1) + acos(t2) + acos(t3)) =
sin(acos(t1))cos(acos(t2) + acos(t3)) + sin(acos(t2)+acos(t3) )cos(acos(t1)=
sqrt(1-t1*t1) * (t2*t3 - sqrt(1-t2*t2)*sqrt(1-t3*t3)) + (sqrt(1-t2*t2) * t3 + sqrt(1-t3*t3) * t2 ) * t1
и т.д.
sqrt() для x в (-1,1) можно вычислить, используя
x_0 is some approximation, say, zero
x_(n+1) = 0.5 * (x_n + S/x_n) where S is the argument.
EDIT: Я имею в виду "вавилонский метод", см. статью в Википедии. Вам потребуется не более 5-6 итераций для достижения 1e-10 с x в (0,1).
Ответ 4
Вы можете попытаться создать таблицы поиска и использовать их вместо стандартных функций С++ и посмотреть, не видите ли вы повышение производительности.
Ответ 5
Значительные выгоды могут быть достигнуты путем выравнивания памяти и потоковой передачи данных в ядре. Чаще всего это затмевает выгоды, которые могут быть достигнуты путем воссоздания математических функций. Подумайте о том, как вы можете улучшить доступ к памяти к/из вашего оператора ядра.
Доступ к памяти может быть улучшен с использованием методов буферизации. Это зависит от вашей аппаратной платформы. Если вы запускаете это на DSP, вы можете DMA ваши данные в кэш L2 и планировать инструкции, чтобы единицы множителя были полностью заняты.
Если вы используете CPU общего назначения, большинство из них вы можете сделать, это использовать выровненные данные, кормить строки кэша путем предварительной выборки. Если у вас есть вложенные циклы, тогда внутренний цикл цикла должен идти туда и обратно (т.е. Итерации вперед, а затем итерации назад), чтобы линии кэша использовались и т.д.
Вы также можете подумать о способах распараллеливания вычислений с использованием нескольких ядер. Если вы можете использовать графический процессор, это может значительно повысить производительность (хотя и с меньшей точностью).
Ответ 6
В дополнение к тому, что говорили другие, здесь приведены некоторые методы оптимизации скорости:
Профиль
Узнайте, где в коде большую часть времени тратится.
Только оптимизируйте эту область, чтобы получить выгоду от мозаики.
Развернуть петли
Процессоры не любят ветки или переходы или изменения в пути выполнения. В общем, процессор должен перезагрузить конвейер команд, который использует время, которое можно потратить на вычисления. Это включает вызовы функций.
Метод состоит в том, чтобы поместить больше "наборов" операций в ваш цикл и уменьшить количество итераций.
Объявить переменные как регистр
Часто используемые переменные должны быть объявлены как register
. Хотя многие члены SO заявили, что компиляторы игнорируют это предложение, я обнаружил в противном случае. Худший случай, вы потратили время на ввод текста.
Сохранять интенсивные вычисления Короткие и простые
Многие процессоры имеют достаточно места в своих конвейерах для хранения небольших петель for
. Это уменьшает количество времени, затрачиваемого на перезагрузку конвейера команд.
Распределите ваш большой цикл вычислений на множество небольших.
Выполнить работу над малыми сегментами массивов и матриц
Многие процессоры имеют кэш данных, который является очень быстрой памятью, очень близкой к процессору. Процессор любит загружать кеш данных один раз из нерабочей памяти. Для большего количества нагрузок требуется время, затрачиваемое на расчеты. Поиск в Интернете для "Кэширования данных, ориентированного на данные".
Подумайте о параллельных условиях процессора
Измените дизайн своих вычислений, чтобы их можно было легко адаптировать для использования с несколькими процессорами. Многие процессоры имеют несколько ядер, которые могут выполнять инструкции параллельно. Некоторые процессоры обладают достаточным интеллектом для автоматического делегирования инструкций их нескольким ядрам.
Некоторые компиляторы могут оптимизировать код для параллельной обработки (найдите параметры компилятора для своего компилятора). Проектирование вашего кода для параллельной обработки облегчит эту оптимизацию для компилятора.
Анализ сборки Список функций
Распечатайте список языков вашей ассемблера.
Измените дизайн своей функции так, чтобы она соответствовала дизайну языка ассемблера или помогла компилятору создать более оптимальный язык ассемблера.
Если вам действительно нужна большая эффективность, оптимизируйте язык ассемблера и введите его как встроенный код сборки или как отдельный модуль. Обычно я предпочитаю последнее.
Примеры
В вашей ситуации возьмите первые 10 членов расширения Тейлора, вычислите их отдельно и поместите в отдельные переменные:
double term1, term2, term3, term4;
double n, n1, n2, n3, n4;
n = 1.0;
for (i = 0; i < 100; ++i)
{
n1 = n + 2;
n2 = n + 4;
n3 = n + 6;
n4 = n + 8;
term1 = 4.0/n;
term2 = 4.0/n1;
term3 = 4.0/n2;
term4 = 4.0/n3;
Затем суммируйте все ваши термины:
result = term1 - term2 + term3 - term4;
// Or try sorting by operation, if possible:
// result = term1 + term3;
// result -= term2 + term4;
n = n4 + 2;
}
Ответ 7
Рассмотрим сначала два члена:
cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)
или cos(a+b) = cos(a)*cos(b) - sqrt(1-cos(a)*cos(a))*sqrt(1-cos(b)*cos(b))
Взяв cos для RHS
a+b = acos( cos(a)*cos(b) - sqrt(1-cos(a)*cos(a))*sqrt(1-cos(b)*cos(b)) )
... 1
Здесь cos (a) = t_1 и cos (b) = t_2
a = acos (t_1) и b = acos (t_2)
Подставив в уравнение (1), получим
acos(t_1) + acos(t_2) = acos(t_1*t_2 - sqrt(1 - t_1*t_1) * sqrt(1 - t_2*t_2))
Здесь вы можете увидеть, что вы объединили два acos в один. Таким образом, вы можете преобразовать все acos рекурсивно и сформировать двоичное дерево. В конце вы останетесь с выражением формы sin(acos(x))
, которая равна sqrt(1 - x*x)
.
Это улучшит временную сложность.
Однако я не уверен в сложности вычисления sqrt().