Как быстрее всего вычислить sin и cos вместе?

100

Я хотел бы вычислить как синус, так и косинус значения вместе (например, для создания матрицы вращения). Конечно, я мог бы вычислить их отдельно, одно за другим a = cos(x); b = sin(x);, но мне интересно, есть ли более быстрый способ, когда нужны оба значения.

Изменить: чтобы обобщить ответы на данный момент:

  • Влад сказал, что есть команда asm, вычисляющая ихFSINCOSобоих (почти одновременно с вызовом вFSINодиночку)

  • Как заметил Чи , эта оптимизация иногда уже выполняется компилятором (при использовании флагов оптимизации).

  • caf указал, что функцииsincosиsincosf, вероятно, доступны и могут быть вызваны напрямую, просто включивmath.h

  • Подход tanascius к использованию справочной таблицы является спорным. (Однако на моем компьютере и в тестовом сценарии он работает в 3 раза быстрее, чемsincosс почти такой же точностью для 32-битных чисел с плавающей запятой.)

  • Джоэл Гудвин связался с интересным подходом к чрезвычайно быстрой технике аппроксимации с довольно хорошей точностью (для меня это даже быстрее, чем поиск в таблице)

Danvil
источник
1
См. Также этот вопрос о собственной реализации sin / cos: stackoverflow.com/questions/1640595
Джоэл Гудвин
1
попробуйте sinx ~ x-x^3/6и cosx~1-x^2/4как приближения, если вам важнее скорость, чем точность. Вы можете добавлять термины в любую серию по мере того, как придаете большее значение точности ( en.wikipedia.org/wiki/Taylor_series прокрутите вниз до серии триггеров Тейлора). Обратите внимание, что это общий способ аппроксимации любой функции, которую вы хотите, с дифференцируемым nвременем. Так что, если у вас есть более крупная функция, которой принадлежат эти синусы и косинусы, вы получите гораздо большую скорость, если вы аппроксимируете ее вместо sin, cos независимо.
ldog
Это плохая техника с очень низкой точностью. См. Сообщение Джоэла Гудвина. Сериалы Тейлора размещены ниже. Пожалуйста, опубликуйте это как ответ.
Danvil
1
Ну, это зависит от ваших требований, если вам нужна точность, серия Тейлора будет хорошим приближением, только если вам нужны значения, xблизкие к некоторой точке x_0, а затем расширьте свою серию Тейлора вокруг x_0вместо 0. Это даст вам отличную точность рядом, x_0но чем дальше вы тем хуже результаты. Вы, вероятно, думали, что точность - отстой, потому что вы смотрели на данный ответ и пробовали его для значений, далеких от 0. Этот ответ с грехом, cos увеличился примерно до 0.
ldog

Ответы:

52

Современные процессоры Intel / AMD имеют инструкции FSINCOSпо одновременному вычислению синусоидальных и косинусных функций. Если вам нужна сильная оптимизация, возможно, вам стоит ее использовать.

Вот небольшой пример: http://home.broadpark.no/~alein/fsincos.html

Вот еще один пример (для MSVC): http://www.codeguru.com/forum/showthread.php?t=328669

Вот еще один пример (с gcc): http://www.allegro.cc/forums/thread/588470

Надеюсь, что один из них поможет. (Сам не использовал эту инструкцию, извините.)

Поскольку они поддерживаются на уровне процессора, я ожидаю, что они будут намного быстрее, чем поиск в таблицах.

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

Изменить:
в документации Intel указано, что FSINCOSэто примерно в 5 раз медленнее, чем FDIV(т.е. деление с плавающей запятой).

Изменить:
обратите внимание, что не все современные компиляторы оптимизируют вычисление синуса и косинуса в вызове FSINCOS. В частности, мой VS 2008 этого не делал.

Изменить:
ссылка на первый пример мертва, но на Wayback Machine все еще есть версия .

Влад
источник
1
@phkahler: Было бы здорово. Не знаю, используется ли такая оптимизация в современных компиляторах.
Влад
12
fsincosИнструкция не «достаточно быстро». В собственном руководстве Intel по оптимизации говорится, что на последних микроархитектурах требуется от 119 до 250 циклов. Математическая библиотека от Intel (распределенный с ICC), путем сравнения, можно отдельно вычислить sinи cosменее чем за 100 циклов, используя программную реализацию , которая использует SSE вместо блока x87. Аналогичная программная реализация, которая вычисляла оба одновременно, могла быть еще быстрее.
Стивен Кэнон
2
@Vlad: Математические библиотеки ICC не являются открытыми, и у меня нет лицензии на их распространение, поэтому я не могу опубликовать сборку. Однако я могу сказать вам, что у них нет встроенных sinвычислений, которыми можно было бы воспользоваться; они используют те же инструкции SSE, что и все остальные. Что касается вашего второго комментария, скорость относительно не fdivимеет значения; если есть два способа сделать что-то и один в два раза быстрее другого, нет смысла называть более медленный «быстрым», независимо от того, сколько времени это занимает по сравнению с какой-то совершенно не связанной задачей.
Стивен Кэнон
1
Программная sinфункция в их библиотеке обеспечивает полную точность с двойной точностью. fsincosИнструкция обеспечивает несколько более высокую точность (двойной продлен), но повышенная точность получает выбрасываются в большинстве программ , которые называют sinфункцию, так как его результат, как правило , округляется до двойной точности попозже арифметических операций или магазин в памяти. В большинстве случаев они обеспечивают одинаковую точность для практического использования.
Стивен Кэнон
4
Также обратите внимание, что fsincosэто не полная реализация сама по себе; вам потребуется дополнительный шаг уменьшения диапазона, чтобы поместить аргумент в допустимый диапазон ввода для fsincosинструкции. Библиотека sinи cosфункции включают это сокращение, а также основные вычисления, поэтому они даже быстрее (по сравнению), чем может указывать время цикла, которое я указал.
Стивен Кэнон
39

В современных процессорах x86 есть инструкция fsincos, которая будет делать именно то, что вы просите - вычислять sin и cos одновременно. Хороший оптимизирующий компилятор должен обнаруживать код, который вычисляет sin и cos для одного и того же значения, и использовать команду fsincos для его выполнения.

Чтобы это сработало, потребовалось немного перевернуть флаги компилятора, но:

$ gcc --version
i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5488)
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ cat main.c
#include <math.h> 

struct Sin_cos {double sin; double cos;};

struct Sin_cos fsincos(double val) {
  struct Sin_cos r;
  r.sin = sin(val);
  r.cos = cos(val);
  return r;
}

$ gcc -c -S -O3 -ffast-math -mfpmath=387 main.c -o main.s

$ cat main.s
    .text
    .align 4,0x90
.globl _fsincos
_fsincos:
    pushl   %ebp
    movl    %esp, %ebp
    fldl    12(%ebp)
    fsincos
    movl    8(%ebp), %eax
    fstpl   8(%eax)
    fstpl   (%eax)
    leave
    ret $4
    .subsections_via_symbols

Тада, пользуется инструкцией fsincos!

Чи
источник
Это здорово! Не могли бы вы объяснить, что делает -mfpmath = 387? А с MSVC тоже работает?
Danvil
1
Обратите внимание на это -ffast-mathи -mfpmathв некоторых случаях приводят к разным результатам.
Debilski
3
mfpmath = 387 заставит gcc использовать инструкции x87 вместо инструкций SSE. Я подозреваю, что MSVC имеет аналогичные оптимизации и флаги, но у меня нет MSVC, чтобы быть уверенным. Использование инструкций x87, скорее всего, снизит производительность в другом коде, хотя вам также следует посмотреть мой другой ответ, чтобы использовать Intel MKL.
Чи
Мой старый gcc 3.4.4 от cygwin производит 2 отдельных вызова fsinи fcos. :-(
Влад
Пробовал с Visual Studio 2008 с включенной максимальной оптимизацией. Он вызывает 2 библиотечные функции __CIsinи __CIcos.
Влад
13

Когда вам нужна производительность, вы можете использовать предварительно вычисленную таблицу sin / cos (подойдет одна таблица, сохраненная в виде словаря). Ну, это зависит от необходимой точности (возможно, таблица будет слишком большой), но это должно быть очень быстро.

Tanascius
источник
Затем входное значение необходимо отобразить на [0,2 * pi] (или меньше с дополнительными проверками), и этот вызов fmod съедает производительность. В моей (вероятно, неоптимальной) реализации мне не удалось добиться производительности с помощью таблицы поиска. У вас есть здесь какой-нибудь совет?
Danvil
11
Предварительно вычисленная таблица почти наверняка будет медленнее, чем просто вызов, sinпотому что предварительно вычисленная таблица будет уничтожать кеш.
Андреас Бринк
1
Это зависит от размера стола. Таблица с 256 записями часто бывает достаточно точной и использует только 1 КБ ... если вы используете ее много, разве она не застревает в кеше, не влияя отрицательно на производительность остальной части приложения?
Мистер Бой
@Danvil: Вот пример синусоидальной таблицы поиска en.wikipedia.org/wiki/Lookup_table#Computing_sines . Однако предполагается, что вы уже сопоставили свой ввод с [0; 2pi].
tanascius
@AndreasBrinck Я бы не пошел так далеко. Это зависит (TM). Современные кэши огромны, а таблицы поиска - маленькими. Довольно часто, если вы немного позаботитесь о компоновке памяти, ваша таблица поиска не должна иметь никакого значения для использования кеша остальной частью ваших вычислений. Тот факт, что таблица поиска умещается внутри кеша, является одной из причин, по которой она работает так быстро. Даже в Java, где сложно точно контролировать макет памяти, я добился огромных успехов в производительности с помощью таблиц поиска.
Джаррод Смит,
13

Технически этого можно добиться, используя комплексные числа и формулу Эйлера . Таким образом, что-то вроде (C ++)

complex<double> res = exp(complex<double>(0, x));
// or equivalent
complex<double> res = polar<double>(1, x);
double sin_x = res.imag();
double cos_x = res.real();

должен дать вам синус и косинус за один шаг. Как это делается внутри, зависит от используемого компилятора и библиотеки. Это может (и может) занять больше времени, чтобы сделать это таким образом (просто потому, что формула Эйлера в основном используется для вычисления сложных expс использованием sinи cos- а не наоборот), но возможна некоторая теоретическая оптимизация.


редактировать

Заголовки в <complex>ГНУ C ++ 4.2 используют явные вычисления sinи cosвнутри polar, поэтому он не выглядит слишком хорошо для оптимизаций там , если компилятор не делает некоторые магии (см -ffast-mathи -mfpmathпереключатели , как написано в ответ Чи ).

Дебильски
источник
извините, но формула Эйлера на самом деле не говорит вам, как что-то вычислить, это просто тождество (хотя и очень полезное), которое связывает сложные экспоненты с реальными тригонометрическими функциями. Есть преимущества одновременного вычисления синуса и косинуса, но они включают общие подвыражения, и в вашем ответе это не обсуждается.
Jason S
12

Вы можете вычислить любое из них, а затем использовать идентификатор:

соз (х) 2 = 1 - грех (х) 2

но, как говорит @tanascius, заранее вычисленная таблица - это путь.

Митч Уит
источник
8
И имейте в виду, что использование этого метода включает вычисление мощности и квадратного корня, поэтому, если важна производительность, убедитесь, что это действительно быстрее, чем вычисление другой триггерной функции напрямую.
Тайлер МакГенри
4
sqrt()часто оптимизируется аппаратно, поэтому вполне может быть быстрее, чем sin()или cos(). Сила - это просто умножение себя, так что не используйте pow(). Есть несколько приемов, позволяющих очень быстро получить достаточно точный квадратный корень без аппаратной поддержки. Наконец, не забудьте профилировать, прежде чем делать что-либо из этого.
deft_code
12
Обратите внимание, что √ (1 - cos ^ 2 x) менее точно, чем вычисление sin x напрямую, в частности, когда x ~ 0.
kennytm
1
Для малых x очень хорош ряд Тейлора для y = sqrt (1-x * x). Вы можете получить хорошую точность с первыми тремя членами, и для этого потребуется всего несколько умножений и один сдвиг. Я использовал это в коде с фиксированной точкой.
phkahler
1
@phkahler: Ваш ряд Тейлора неприменим, потому что когда x ~ 0, cos x ~ 1.
kennytm
10

Если вы используете библиотеку GNU C, вы можете:

#define _GNU_SOURCE
#include <math.h>

и вы получите объявления функций sincos(), sincosf()и, sincosl()которые вычисляют оба значения вместе - вероятно, самым быстрым способом для вашей целевой архитектуры.

кафе
источник
8

На этой странице форума есть очень интересные материалы, которые ориентированы на быстрый поиск хороших приближений: http://www.devmaster.net/forums/showthread.php?t=5784

Отказ от ответственности: Я не использовал ничего из этого.

Обновление от 22 февраля 2018 г .: Wayback Machine - единственный способ посетить исходную страницу сейчас: https://web.archive.org/web/20130927121234/http://devmaster.net/posts/9648/fast-and-accurate- синус-косинус

Джоэл Гудвин
источник
Я попробовал и этот, и он дал мне неплохие результаты. Но sin и cos вычисляются независимо.
Danvil
Я чувствую, что этот расчет синуса / косинуса будет быстрее, чем получение синуса и использование приближения квадратного корня для получения косинуса, но тест подтвердит это. Первичная связь между синусом и косинусом - это одна фаза; Можно ли закодировать так, чтобы вы могли повторно использовать значения синуса, которые вы вычисляете для вызовов косинуса со сдвигом по фазе, принимая это во внимание? (Это может быть натяжка, но пришлось спросить)
Джоэл Гудвин
Не прямо (несмотря на то, что вопрос задает именно это). Мне нужны sin и cos значения x, и нет способа узнать, случайно ли в каком-то другом месте я вычислил x + pi / 2 ...
Данвил
Я использовал его в своей игре, чтобы нарисовать круг из частиц. Поскольку это всего лишь визуальный эффект, результат достаточно близок, а производительность действительно впечатляет.
Максим Камалов
Я не впечатлен; Чебышевские приближения обычно дают наибольшую точность для заданной производительности.
Jason S
7

Многие математические библиотеки C, как указывает caf, уже имеют sincos (). Заметным исключением является MSVC.

  • У Sun есть sincos () по крайней мере с 1987 года (двадцать три года; у меня есть бумажная страница руководства)
  • У HPUX 11 он был в 1997 году (но его нет в HPUX 10.20)
  • Добавлен в glibc в версии 2.1 (февраль 1999 г.)
  • Стал встроенным в gcc 3.4 (2004), __builtin_sincos ().

Что касается поиска, Эрик С. Реймонд в книге « Искусство программирования Unix» (2004) (глава 12) прямо говорит, что это плохая идея (в настоящий момент):

Другой пример - предварительное вычисление небольших таблиц - например, таблица sin (x) по градусам для оптимизации вращений в движке трехмерной графики займет 365 × 4 байта на современной машине. До того, как процессоры стали достаточно быстрее памяти, чтобы требовать кэширования , это была очевидная оптимизация скорости.В настоящее время может быть быстрее каждый раз пересчитывать, чем платить за процент дополнительных промахов в кэше, вызванных таблицей.

«Но в будущем это может измениться снова, когда кеши станут больше. В целом, многие оптимизации являются временными и могут легко превратиться в пессимизацию при изменении соотношений затрат. Единственный способ узнать - это измерить и увидеть». (из Искусства программирования Unix )

Но, судя по изложенному выше, не все согласны.

Джозеф Куинси
источник
10
«365 х 4 байта». Вам нужно учитывать високосные годы, так что на самом деле это должно быть 365,25 x 4 байта. Или, может быть, он имел в виду использовать количество градусов в круге вместо количества дней в земном году.
Ponkadoodle
@Wallacoloo: Хорошее наблюдение. Я скучаю по этому. Но ошибка в оригинале .
Джозеф Куинси
РЖУНИМАГУ. Кроме того, он пренебрегает тем фактом, что во многих компьютерных играх в этой области вам понадобится только конечное количество углов. Тогда промахов кеша не будет, если вы знаете возможные углы. Я бы использовал таблицы именно в этом случае и fsincosпопробовал бы (инструкция ЦП!) Для остальных. Часто это так же быстро, как интерполяция sin и cos из большой таблицы.
Эрих Шуберт
5

Я не верю, что справочные таблицы обязательно являются хорошей идеей для решения этой проблемы. Если ваши требования к точности не очень низкие, таблица должна быть очень большой. А современные процессоры могут выполнять много вычислений, пока значение извлекается из основной памяти. Это не один из тех вопросов, на которые можно правильно ответить с помощью аргументов (даже не моих), тестирования, измерения и рассмотрения данных.

Но я бы посмотрел на быстрые реализации SinCos, которые вы найдете в таких библиотеках, как AMD ACML и Intel MKL.

Знак высокой эффективности
источник
3

Если вы хотите использовать коммерческий продукт и одновременно рассчитываете несколько вычислений sin / cos (чтобы вы могли использовать векторные функции), вам следует обратиться к библиотеке Intel Math Kernel Library.

Имеет функцию синкос

Согласно этой документации, он в среднем составляет 13,08 тактов на элемент на дуэте Core 2 в режиме высокой точности, что, я думаю, будет даже быстрее, чем fsincos.

Чи
источник
1
Точно так же в OSX можно использовать Accelerate.framework vvsincosили vvsincosfиз него. Я считаю, что у AMD есть аналогичные функции в своей векторной библиотеке.
Стивен Кэнон
3

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

Уловка DSP: одновременное параболическое приближение Sin и Cos

http://www.dspguru.com/dsp/tricks/parabolic-approximation-of-sin-and-cos

Зонды
источник
1
хммм ... Мне нужно провести перестрелку между этим приближением и приближением Чебышева, которое, я думаю, победит.
Jason S
2

Когда производительность критична для такого рода вещей, нередко вводится таблица поиска.

Том Кабански
источник
2

Что касается творческого подхода, как насчет расширения серии Тейлора? Поскольку у них похожие термины, вы можете сделать что-то вроде следующего псевдонима:

numerator = x
denominator = 1
sine = x
cosine = 1
op = -1
fact = 1

while (not enough precision) {
    fact++
    denominator *= fact
    numerator *= x

    cosine += op * numerator / denominator

    fact++
    denominator *= fact
    numerator *= x

    sine += op * numerator / denominator

    op *= -1
}

Это означает, что вы делаете что-то вроде этого: начиная с x и 1 для sin и косинуса, следуйте шаблону - вычтите x ^ 2/2! из косинуса вычтите x ^ 3/3! из синуса добавить x ^ 4/4! к косинусу прибавить x ^ 5/5! синус ...

Я понятия не имею, будет ли это работать. Если вам нужна меньшая точность, чем дают встроенные функции sin () и cos (), это может быть вариантом.

Тессерекс
источник
Фактически, коэффициент расширения i-синуса в x / i умножен на коэффициент расширения i-косинуса. Но я сомневаюсь, что использование серии Тейлора действительно быстро ...
Данвил
1
Чебышев намного лучше Тейлора для приближения полиномиальных функций. Не используйте приближение Тейлора.
Timmmm
Здесь есть несколько числовых ошибок; числитель и знаменатель быстро становятся большими, что приводит к ошибкам с плавающей запятой. Не говоря уже о том, как вы решаете, что такое «недостаточная точность» и как ее рассчитывать? Приближение Тейлора хорошо в окрестности одной точки; вдали от этой точки они быстро становятся неточными и требуют большого количества членов, поэтому предложение Тимммма о приближении Чебышева (которое создает хорошие приближения на заданном интервале) является хорошим.
Jason S
2

В библиотеке CEPHES есть хорошее решение, которое может быть довольно быстрым, и вы можете довольно гибко добавлять / удалять точность, немного больше / меньше процессорного времени.

Помните, что cos (x) и sin (x) - это действительная и мнимая части exp (ix). Итак, мы хотим вычислить exp (ix), чтобы получить и то, и другое. Мы предварительно вычисляем exp (iy) для некоторых дискретных значений y между 0 и 2pi. Сдвигаем x на интервал [0, 2pi). Затем мы выбираем y, ближайший к x, и пишем
exp (ix) = exp (iy + (ix-iy)) = exp (iy) exp (i (xy)).

Мы получаем exp (iy) из справочной таблицы. А поскольку | xy | мала (не более половины расстояния между значениями y), ряд Тейлора будет хорошо сходиться всего за несколько членов, поэтому мы используем это для exp (i (xy)). И тогда нам просто нужно комплексное умножение, чтобы получить exp (ix).

Еще одним приятным свойством этого является то, что вы можете векторизовать его с помощью SSE.

Jsl
источник
2

Возможно, вы захотите взглянуть на http://gruntthepeon.free.fr/ssemath/ , который предлагает векторизованную реализацию SSE, вдохновленную библиотекой CEPHES. Он имеет хорошую точность (максимальное отклонение от sin / cos порядка 5e-8) и скорость (немного превосходит fsincos на основе одного вызова и явный победитель над несколькими значениями).

SleuthEye
источник
1

Точное, но быстрое приближение функций sin и cos одновременно в javascript можно найти здесь: http://danisraelmalta.github.io/Fmath/ (легко импортируется в c / c ++)

user2781980
источник
0

Думали ли вы об объявлении таблиц поиска для двух функций? Вам все равно придется «вычислять» sin (x) и cos (x), но это будет значительно быстрее, если вам не нужна высокая степень точности.

Фрэнк Шеарар
источник
0

Компилятор MSVC может использовать (внутренние) функции SSE2

 ___libm_sse2_sincos_ (for x86)
 __libm_sse2_sincos_  (for x64)

в оптимизированных сборках, если указаны соответствующие флаги компилятора (как минимум / O2 / arch: SSE2 / fp: fast). Имена этих функций, по-видимому, подразумевают, что они вычисляют не отдельные sin и cos, а вычисляют обе «за один шаг».

Например:

void sincos(double const x, double & s, double & c)
{
  s = std::sin(x);
  c = std::cos(x);
}

Сборка (для x86) с / fp: fast:

movsd   xmm0, QWORD PTR _x$[esp-4]
call    ___libm_sse2_sincos_
mov     eax, DWORD PTR _s$[esp-4]
movsd   QWORD PTR [eax], xmm0
mov     eax, DWORD PTR _c$[esp-4]
shufpd  xmm0, xmm0, 1
movsd   QWORD PTR [eax], xmm0
ret     0

Сборка (для x86) без / fp: fast, но с / fp: precision вместо этого (что по умолчанию) вызывает отдельные sin и cos:

movsd   xmm0, QWORD PTR _x$[esp-4]
call    __libm_sse2_sin_precise
mov     eax, DWORD PTR _s$[esp-4]
movsd   QWORD PTR [eax], xmm0
movsd   xmm0, QWORD PTR _x$[esp-4]
call    __libm_sse2_cos_precise
mov     eax, DWORD PTR _c$[esp-4]
movsd   QWORD PTR [eax], xmm0
ret     0

Итак, / fp: fast является обязательным для оптимизации sincos.

Но учтите, что

___libm_sse2_sincos_

может быть не так точно, как

__libm_sse2_sin_precise
__libm_sse2_cos_precise

из-за отсутствия «точного» в конце названия.

На моей «немного» более старой системе (Intel Core 2 Duo E6750) с последним компилятором MSVC 2019 и соответствующими оптимизациями мой тест показывает, что вызов sincos примерно в 2,4 раза быстрее, чем отдельные вызовы sin и cos.

ху
источник