Быстрая реализация тригонометрических функций для c++



Короткая версия: я хотел бы знать, существуют ли реализации стандартных тригонометрических функций, которые быстрее, чем те, которые включены в math.h.



Длинная версия: у меня есть программа, которая довольно тяжелая на цифрах (это физическая симуляция) и которая должна вызывать тригонометрические функции, в основном sin и cos, много. В настоящее время я просто использую реализации, включенные в math.h. Профилирование показывает, что вызовы этих функций стоят больше, чем я ожидал (надеявшийся).



Хотя в других частях кода, безусловно, есть много места для оптимизации, более быстрые sin и cos могут дать мне некоторые дополнительные проценты.. Итак, у вас есть какие-нибудь предложения?

В другом посте предлагается использовать самодельные таблицы подстановки. Но, может быть, есть альтернативы? Или готовые и хорошо протестированные решения поиска в некоторых библиотеках?

2207   10  

10 ответов:

Вот несколько хороших слайдов о том, как делать аппроксимации степенных рядов (но не рядов Тейлора) тригонометрических функций: http://www.research.scea.com/gdc2003/fast-math-functions.html

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

Хорошая вещь в этом заключается в том, что вы также должны быть в состоянии легко расширить его на SIMD, таким образом, вы можете вычислить sin или cos из 4 значений в одном (2, Если вы используете двойную точность).

Надеюсь, это поможет...

Это должно быть чертовски быстро, если вы можете оптимизировать его дальше, пожалуйста, сделайте и опубликуйте код на like pastie.org или еще что-нибудь.

Технические характеристики компьютера - > 512 МБ оперативной памяти, Visual Studio 2010, Windows XP Professional SP3 версии 2002, Процессор Intel (R) Pentium (R) 4 2.8 ГГц.

Это безумно точно и фактически обеспечит несколько лучшие результаты в некоторых ситуациях. Например, 90, 180, 270 градусов в C++ возвращает не 0 десятичное число.

Полная таблица от 0 до 359 градусов: https://pastee.org/dhwbj

Формат - > степень # - > MINE_X (#), CosX (#), MINE_Z (#), SinZ (#).

Ниже приведен код, используемый для построения приведенной выше таблицы. Вероятно, вы можете сделать его еще более точным, если используете более крупный тип данных. Я использовал неподписанный шорт и сделал N/64000. Так что когда-либо cos (##) и sin (##), где ближе всего к I округлены до этого индекса. Я также старался использовать как можно меньше дополнительных данных, чтобы это не была какая-то захламленная таблица с 720 float значения для cos и sin. Что, вероятно, дало бы лучшие результаты, но было бы полной тратой памяти. Таблица ниже настолько мала, насколько я мог ее сделать. Я хотел бы посмотреть, возможно ли составить уравнение, которое могло бы округлить все эти короткие значения и использовать их вместо этого. Я не уверен, что это будет быстрее, но это полностью устранит таблицу и, вероятно, не уменьшит скорость ни на что или много.

Таким образом, точность по сравнению с операциями Cos/sin C++ составляет 99,99998% через 100%.

Ниже приведена таблица, используемая для вычисления значений cos/sin.
static const unsigned __int16 DEGREE_LOOKUP_TABLE[91] =
{
    64000, 63990, 63961, 63912, 63844, 63756,
    63649, 63523, 63377, 63212, 63028, 62824,
    62601, 62360, 62099, 61819, 61521, 61204,
    60868, 60513, 60140, 59749, 59340, 58912,
    58467, 58004, 57523, 57024, 56509, 55976,
    55426, 54859, 54275, 53675, 53058, 52426,
    51777, 51113, 50433, 49737, 49027, 48301,
    47561, 46807, 46038, 45255, 44458, 43648,
    42824, 41988, 41138, 40277, 39402, 38516,
    37618, 36709, 35788, 34857, 33915, 32962,
    32000, 31028, 30046, 29055, 28056, 27048,
    26031, 25007, 23975, 22936, 21889, 20836,
    19777, 18712, 17641, 16564, 15483, 14397,
    13306, 12212, 11113, 10012,  8907,  7800,
     6690,  5578,  4464,  3350,  2234,  1117,
        0,
};
Ниже приведен фактический код, который выполняет вычисления cos/sin.
    int deg1 = (int)degrees;
    int deg2 = 90 - deg1;
    float module = degrees - deg1;
    double vX = DEGREE_LOOKUP_TABLE[deg1] * 0.000015625;
    double vZ = DEGREE_LOOKUP_TABLE[deg2] * 0.000015625;
    double mX = DEGREE_LOOKUP_TABLE[deg1 + 1] * 0.000015625;
    double mZ = DEGREE_LOOKUP_TABLE[deg2 - 1] * 0.000015625;
    float vectorX = vX + (mX - vX) * module;
    float vectorZ = vZ + (mZ - vZ) * module;
    if (quadrant & 1)
    {
        float tmp = vectorX;
        if (quadrant == 1)
        {
            vectorX = -vectorZ;
            vectorZ = tmp;
        } else {
            vectorX = vectorZ;
            vectorZ = -tmp;
        }
    } else if (quadrant == 2) {
        vectorX = -vectorX;
        vectorZ = -vectorZ;
    }

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

МОЙ МЕТОД

1,000 Iterations -> 0.004641 MS or 4641 NanoSeconds.
100,000 Iterations -> 4.4328 MS.
100,000,000 Iterations -> 454.079 MS.
1,000,000,000 Iterations -> 4065.19 MS.

МЕТОД COS / SIN

1,000 Iterations -> 0.581016 MS or 581016 NanoSeconds.
100,000 Iterations -> 25.0049 MS.
100,000,000 Iterations -> 24,731.6 MS.
1,000,000,000 Iterations -> 246,096 MS.

Итак, подведем итог вышесказанному. выполнение cos (###) и sin (###) с помощью моей стратегии позволяет примерно 220 000 000 исполнений в секунду. Используя спецификации компьютера, показанные первоначально. Это довольно быстро и использует очень мало памяти, так что это отличная замена математических функций cos/sin, обычно встречающихся в C++. Если вы хотите увидеть точность, откройте ссылку, показанную выше,и там есть печать из градусов 0 по 359. Кроме того, это поддерживает от 0 до 89 и квадранты от 0 до 3. Так что вам нужно либо использовать это, либо выполните (Градусы % 90).

Источник Quake 3 имеет некоторый код для предварительно вычисленных sine/cos, нацеленный на скорость над точностью, его не sse, основанный на том, что таким образом вполне переносим(как на архитектуре, так и на встроенном api). Вы также можете найти это резюме функций на основе SSE и sse2 очень интересным: http://gruntthepeon.free.fr/ssemath/

Если вы хотите использовать пользовательскую реализацию, смотрите здесь, здесь и здесь

Также здесь (Перейдите к универсальной SIMD-Mathlibrary), если вам нужно вычислить sin/cos для больших массивов

Можно также попробовать использовать встроенные компоненты SSE C++. Смотрите здесь

Обратите внимание, что большинство современных компиляторов поддерживают оптимизацию SSE и SSE2. Например, для Visual Studio 2010 его необходимо включить вручную. Как только вы сделаете это, другой реализация будет использоваться для большинства стандартных математических функций.

Еще один вариант - использовать DirectX HLSL. Посмотрите сюда . Обратите внимание, что есть хорошие функции sincos, которые возвращают как sin, так и cos.

Обычно я использую IPP (который не является бесплатным). Подробнее смотрите здесь

А) попытка сэкономить небольшие проценты не принесет большого удовлетворения. Финиш в 97 вместо 100 часов-это еще долго.

B) вы говорите, что профилировали, и что тригонометрические функции занимают больше времени, чем вам хотелось бы. И сколько же? а как насчет всего остального времени? Вполне возможно, что у вас есть более крупная рыба для жарки. Большинство профилировщиков , основанных на концепциях gprof, не сообщают вам о вызовах среднего стека, на которых вы могли бы сосредоточиться, чтобы сэкономить больше времени. Вот пример: образец.

Я реализовал быструю функцию синуса на стороне процессора, которая по крайней мере в два раза быстрее, чем математика.однако я использовал очень маленькую таблицу поиска (20 поплавков). его точность также не так уж плоха; средняя относительная погрешность составляет 0,095%. вы можете проверить это из http://www.hevi.info/tag/fast-sine-function/

Объяснение метода довольно простое и опирается на тот факт, что для малых a sin (a) = a * pi / 180 (см. ссылку выше для доказательство)

Введите описание изображения здесь

Немного Тригонометрии

Хотя можно достичь относительно точных результатов с помощью формулы, показанной выше для углов между 0 и 10, поскольку угол становится шире, поскольку он теряет точность. Поэтому мы должны использовать формулу для углов меньше 10, но как?!

Ответ приходит из тригонометрической формулы сложения синусов;

Грех(а+б) = Син(а) потому что(б) + грех(Б) потому что(A)

Если мы можем держать ‘b ' меньше 10, то мы сможем используйте нашу формулу для того, чтобы найти синус с помощью пары аритметических операций.

Предположим, что нам задано значение синуса для 71,654, тогда;

A = 70

B = 1,654

И,

Грех(71.654) = грех(70 + 1.654) = грех(70) потому что(1.654) + грех(1.654), потому что (70)

В этой формуле мы можем использовать быстрый расчет для части sin (1.654), а для остальных, к сожалению, нам нужны таблицы синусов и косинусов. Хорошо то, что нам нужно только умножить десятки для синуса и натуральное число углов между 0 и 10 для Косинуса.

Давным-давно на медленных машинах люди использовали массивы с заранее вычисленными значениями. другой вариант для вычисления с вашей собственной точностью, как это: (ищите "определения рядов")

Вы можете посмотреть на это. Это говорит об оптимизации греха, cos.

Для 2-3% выигрыша это почти наверняка не стоит риска неточности, ошибки, предположений, которые больше не являются истинными (например, никогда не выходят за пределы [-1,-1]) и т. д., если только вы не планируете запустить это на огромном количестве машин (где 2-3% представляют тысячи или миллионы долларов в электроэнергии и амортизированной стоимости машины).

Тем не менее, если у вас есть знание предметной области о том, чего вы пытаетесь достичь, вы можете ускорить свои вычисления на коэффициент два или больше. Например, если вам всегда нужны sin и cos одного и того же значения, вычислите их близко друг к другу в коде и убедитесь, что ваш компилятор переводит их в инструкцию сборки FSINCOS (см. этот вопрос). Если вам нужна только небольшая часть полного диапазона функции, вы можете потенциально использовать набор полиномов низкого порядка с последующей итерацией метода Ньютона для получения полной точности машины (или столько, сколько вам нужно). Опять же, это очень много более мощный, если вы знаете, что вам нужны только некоторые значения-например, если вы можете использовать, что sin(x) близок к x около нуля, и вам будут нужны только значения около нуля, тогда вы можете резко уменьшить количество нужных вам терминов.

Но, опять же, мой главный совет: 2-3% не стоит того. Подумайте лучше об используемых алгоритмах и других потенциальных узких местах (например, не ест ли Мэллок слишком много времени?) прежде чем вы оптимизируете это.

Comments

  1. Костя
    Костя 4 года назад
    <p>Всё зависит от требуемой точности: чем она меньше, тем функция будет быстрее. Для вычислений с одинарной точностью, как правило, требуется многочлен Чебышёва с 5-ю слагаемыми. Такая функция будет работать быстрее, чем любая стандартная реализация, и даже, может быть, быстрее, чем реализация с помощью таблицы и интерполяции. Для вычислений с двойной точностью наилучшая производительность достигается методом Паде-Чебышёва. Скорее всего, она тоже будет превосходить производительность стандартных библиотечных функций, особенно если использовать в коде инструкции из набора FMA. Но для построения подобных аппроксимаций требуются определённые навыки.</p>

    <p>В качестве примера приведу код для функции тангенс аргумента, заданного в градусах. Используются одинарная точность и SSE-инструкции. Здесь b0, ..., b4 - коэффициенты степенного ряда, полученного путём приведения подобных в частичной сумме ряда Чебышёва для функции f(t)=t/tg(pi*t/4), |t|&lt;=1.</p>

    <p> </p>

    <p>_declspec(naked) float _vectorcall tand(float x) // Тангенс в градусах<br />
    {<br />
      static const float ct[6] = // Таблица констант<br />
      {<br />
        -0.0111111111f,               // -1/90 - множитель для приведения x<br />
        57.2957793f,                  // b0*45<br />
        -5.81775793E-3f,              // b1/45<br />
        -1.18170581E-7f,              // b2/45^3<br />
        -3.39436425E-12f,             // b3/45^5<br />
        -1.22505855E-16f              // b4/45^7<br />
      };<br />
      _asm<br />
      {<br />
        mov edx, offset ct            // В edx - адрес таблицы констант<br />
        movss xmm1, [edx]             // xmm1 = -1/90<br />
        mulss xmm1, xmm0              // xmm1 = -x/90<br />
        cvtss2si ecx, xmm1            // ecx = -k, где k - округлённое до целых значение x/90<br />
        imul ecx, 90                  // ecx = -90*k<br />
        jno tg_cont                   // В случае слишком большого |x| считать, как при x=0<br />
        sub ecx, ecx                  // Для этого обнуляем ecx,<br />
        xorps xmm0, xmm0              // и обнуляем xmm0<br />
          tg_cont:                    // Продолжаем для корректного значения x<br />
        cvtsi2ss xmm2, ecx            // xmm2 = -90*k<br />
        shl ecx, 30                   // При нечётном k установить знаковый бит ecx<br />
        addss xmm2, xmm0              // xmm2 = x-90*k = 45*t - в диапазоне [-45; 45]<br />
        movd xmm1, ecx                // В xmm1 - знаковая маска результата<br />
        movss xmm0, [edx+20]          // xmm0 = b4/45^7 - инициализировать сумму<br />
        xorps xmm1, xmm2              // xmm1=(-1)^k*45*t - выставить правильный знак будущего результата<br />
        mulss xmm2, xmm2              // xmm2=(45*t)^2 - подготовить аргумент для вычисления многочлена<br />
        mov cl, 4                     // ecx = 4 или 80000004h - инициализировать счётчик цикла<br />
          tg_loop:                    // Цикл вычисления многочлена по схеме Горнера<br />
        mulss xmm0, xmm2              // Умножить последний результат на аргумент многочлена<br />
        addss xmm0, [edx+ecx*4]       // Прибавить текущий коэффициент<br />
        _emit 0x67                    // Префикс изменения разрядности для получения команды loopw<br />
        loop tg_loop                  // Учесть все 5 коэффициентов. В xmm0 формируется 45*f(t)<br />
        jnz tg_div                    // Перейти для вычисления -f(t)/t, если k нечётное, т.е. |tg x|&gt;1<br />
        movaps xmm2, xmm0             // Для чётного k обменять местами регистры xmm0 и xmm1<br />
        movaps xmm0, xmm1             // В этом случае |tg x|&lt;=1, и нужно считать t/f(t)<br />
        movaps xmm1, xmm2             // Теперь xmm1 = 45*f(t), xmm0 = 45*t<br />
          tg_div:                     // Получение окончательного результата<br />
        divss xmm0, xmm1              // Найти -f(t)/t, если k нечётное, или t/f(t), если k чётное<br />
        ret                           // Вернуться<br />
      }<br />
    }</p>

    <p> </p>