Який найшвидший спосіб обчислити гріх і союз разом?


100

Я хотів би разом обчислити як синус, так і косинус значення (наприклад, створити матрицю обертання). Звичайно, я міг би їх обчислити окремо один за одним, як a = cos(x); b = sin(x);, але мені цікаво, чи існує швидший спосіб, коли потрібні обидва значення.

Редагувати: Щоб узагальнити відповіді поки що:

  • Влад сказав, що є команда asm, якаFSINCOSобчислює їх обох (майже в той самий час, що і дзвінок наFSINсамоту)

  • Як зауважив Chi , цю оптимізацію іноді вже робить компілятор (при використанні оптимізаційних прапорів).

  • CAF зазначив, що функціїsincosіsincosf, ймовірнодоступні і можуть бути викликані безпосередньо тількитому числіmath.h

  • Танаційний підхід використання оглядової таблиці обговорюється суперечливо. (Однак на моєму комп’ютері та в еталонній ситуації він працює в 3 рази швидше, ніжsincosмайже з однаковою точністю для 32-бітних плаваючих точок.)

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


1
Дивіться також це питання про власну реалізацію гріха / cos: stackoverflow.com/questions/1640595
Джоел Гудвін

1
спробуйте sinx ~ x-x^3/6і cosx~1-x^2/4як приближення, якщо вам важлива швидкість більше, ніж точність. Ви можете додавати умови в будь-якій серії, оскільки ви додасте більше ваги на точність ( en.wikipedia.org/wiki/Taylor_series прокрутіть вниз до трійчастого ряду Тейлора.) Зауважте, що це загальний спосіб наблизити будь-яку функцію, яку ви хочете, в різні nчаси. Отже, якщо у вас є якась більша функція, яка належить цим синусам і косинусом, ви отримаєте набагато більшу швидкість, якщо наблизити її замість гріха, а не незалежно.
ldog

Це погана техніка з дуже низькою точністю. Дивіться пост Джоела Гудвіна. Серії Тейлора були розміщені нижче. Будь ласка, опублікуйте це як відповідь.
Данвіл

1
Ну, це залежить від ваших вимог, якщо ви хочете, щоб точність серії Taylor була хорошим наближенням, лише якщо вам потрібні значення, xблизькі до якоїсь точки x_0, а потім розгорніть свою серію Taylor навколо, x_0а не 0. Це дасть вам чудову точність поблизу, x_0але чим далі ви гірші результати. Ви, напевно, думали, що точність висмоктування викликає, коли ви дивитесь на заданий знак і намагаєтесь його визначити далеко не значення 0. Ця відповідь є гріхом,
оскільки

Відповіді:


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 все ще є версія .


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?
Данвіл

1
Зверніть увагу , що -ffast-mathі -mfpmathпризводить до різних результатів в деяких випадках.
Дебільські

3
mfpmath = 387 змусить gcc використовувати x87 інструкції замість SSE. Я підозрюю, що MSVC має подібні оптимізації та прапори, але я не маю під рукою MSVC, щоб бути впевненим. Використання інструкцій x87, ймовірно, буде шкодою для роботи в іншому коді, однак, ви також повинні переглянути мою іншу відповідь, щоб використовувати MKL від Intel.
Чи

Мій старий gcc 3.4.4 від cygwin виробляє 2 окремі дзвінки на fsinта fcos. :-(
Влад

Пробували з Visual Studio 2008 з найвищими оптимізаціями. Він викликає 2 функції бібліотеки __CIsinта __CIcos.
Влад

13

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


Тоді вхідне значення потрібно відобразити на [0,2 * pi] (або менше за допомогою додаткових перевірок), і цей виклик fmod зводить продуктивність. У моїй (можливо, неоптимальній) реалізації я не зміг досягти продуктивності за допомогою таблиці пошуку. Ви б тут мали поради?
Данвіл

11
Попередньо обчислена таблиця майже напевно буде повільнішою, ніж просто виклик, sinтому що попередньо обчислена таблиця буде переносити кеш.
Андреас Брінк

1
Це залежить, наскільки великий стіл. Таблиця з 256 записами часто досить точна і використовує лише 1 Кб ... якщо ви її багато використовуєте, чи не застрягла б вона в кеш-пам'яті, не вплинувши негативно на решту роботи програми?
Містер Хлопчик

@Danvil: Ось приклад таблиці пошуку синусів en.wikipedia.org/wiki/Lookup_table#Computing_sines . Однак передбачається, що ви вже відобразили свій внесок у [0; 2пі].
танацій

@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>для GNU C ++ 4.2 використовуються чіткі обчислення sinі cosвсередині polar, тому це не дуже добре для оптимізацій там, якщо компілятор не зробить якусь магію (див. -ffast-mathТа -mfpmathперемикачі, як написано у відповіді Чи ).


Вибачте, але Формула Ейлера насправді не говорить вам, як щось обчислити, це лише ідентичність (хоч і дуже корисна), яка пов'язує складні експоненти з реальними тригонометричними функціями. Є користь обчислення синуса і косинуса разом, але вони передбачають загальні підекспресії, і ваша відповідь не обговорює це.
Jason S

12

Ви можете обчислити або використати ідентифікацію:

cos (x) 2 = 1 - sin (x) 2

але як говорить @tanascius, попередньо обчислена таблиця - це шлях.


8
І майте на увазі, що використання цього методу передбачає обчислення потужності та квадратного кореня, тому, якщо продуктивність важлива, переконайтеся, що це насправді швидше, ніж обчислення іншої триггерної функції безпосередньо.
Тайлер Макенрі

4
sqrt()часто оптимізовано в апаратному забезпеченні, тому це може бути дуже швидким sin()або cos(). Потужність - це саморозмноження, тому не використовуйте pow(). Є кілька хитрощів, щоб отримати досить точні квадратні корені дуже швидко без апаратної підтримки. Нарешті, не забудьте профайлювати, перш ніж робити щось із цього.
deft_code

12
Зауважимо, що √ (1 - cos ^ 2 x) менш точний, ніж обчислення sin x безпосередньо, зокрема, коли x ~ 0.
kennytm

1
Для малого х серія Тейлора для 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- синус-косинус


Я спробував і цей, і це дало мені досить хороші показники. Але гріх і cos обчислюються незалежно.
Данвіл

Моє відчуття: цей розрахунок синуса / косинуса буде швидшим, ніж отримання синуса та використання наближення квадратного кореня, щоб отримати косинус, але тест підтвердить це. Первинний зв'язок між синусом і косинусом є однією з фаз; чи можна кодувати, щоб ви могли повторно використовувати значення синусів, які ви обчислюєте, для викликів косинуса з фазою, змінених, враховуючи це? (Це може бути розтягнення, але доведеться запитати)
Джоел Гудвін

Не безпосередньо (незважаючи на запитання саме цього). Мені потрібен гріх і cos значення x, і немає ніякого способу дізнатися, чи я в якомусь іншому місці випадково обчислював x + pi / 2 ...
Danvil

Я використовував це в своїй грі, щоб намалювати коло частинок. Оскільки це лише візуальний ефект, результат є досить близьким, а виконанні дійсно вражаючі.
Максим Камалов

Я не вражений; Наближення Чебишева зазвичай дають найбільшу точність для даної роботи.
Jason S

7

У багатьох бібліотеках математики C, як вказує caf, вже є sincos (). Помітний виняток - MSVC.

  • У Sun є синкос () щонайменше з 1987 року (двадцять три роки; у мене є сторінка людини на папері)
  • HPUX 11 мав це в 1997 році (але це не в HPUX 10.20)
  • Додано до glibc у версії 2.1 (лютий 1999)
  • Став вбудованим в gcc 3.4 (2004), __builtin_sincos ().

Щодо пошуку, Ерік С. Реймонд у програмі « Мистецтво програмування Unix» (2004 р.) (Глава 12) прямо говорить про це «Погана ідея» (на даний момент часу):

"Інший приклад - попередньо обчислити невеликі таблиці - наприклад, таблиця sin (x) за ступенем для оптимізації обертів у 3D графічному двигуні займе 365 × 4 байти на сучасній машині. Перш ніж процесори отримали достатньо швидкість, ніж пам'ять, щоб вимагати кешування це було очевидною оптимізацією швидкості. Сьогодні може бути швидше перераховувати кожен раз, а не платити за відсоток додаткових пропусків кешу, викликаних таблицею.

"Але в майбутньому це може знову повернутися, коли кеші збільшуються. Більш загально, багато оптимізацій є тимчасовими і можуть легко перетворитися на песимізацію, оскільки змінюються коефіцієнти витрат. Єдиний спосіб знати - це вимірювати та бачити". (з мистецтва програмування Unix )

Але, судячи з обговорення вище, не всі згодні.


10
"365 х 4 байти". Вам потрібно враховувати високосні роки, так що насправді має бути 365,25 х 4 байти. А може, він мав на увазі використовувати кількість градусів у колі замість кількості днів у земному році.
Ponkadoodle

@Wallacoloo: Приємне спостереження. Я пропустив це. Але помилка є в оригіналі .
Джозеф Квінсі

ЛОЛ. Крім того, він нехтує тим фактом, що в багатьох комп’ютерних іграх тієї області вам знадобиться лише обмежена кількість кутів. Тоді немає пропусків кеша, якщо ви знаєте можливі кути. Я б використовував таблиці саме в цьому випадку, і fsincosспробую (інструкція CPU!) Спробувати для інших. Це часто настільки швидко, як інтерполяція гріха і cos з великого столу.
Еріх Шуберт

5

Я не вірю, що таблиці пошуку обов'язково є хорошою ідеєю для цієї проблеми. Якщо ваші вимоги до точності дуже низькі, стіл повинен бути дуже великим. І сучасні процесори можуть робити багато обчислень, тоді як значення отримується з основної пам'яті. Це не одне з тих питань, на які можна правильно відповісти аргументом (навіть не моїм), перевірити та виміряти та врахувати дані.

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


3

Якщо ви готові використовувати комерційний продукт і одночасно обчислюєте кількість обчислень greh / cos (щоб ви могли використовувати векторовані функції), слід ознайомитися з бібліотекою математичних ядер Intel від Intel.

Він має функцію sincos

Згідно з цією документацією, він в середньому складає 13,08 годин / елемент на дуеті 2 в режимі високої точності, що, думаю, буде навіть швидше, ніж у fsincos.


1
Аналогічно, на OSX можна використовувати vvsincosабо vvsincosfз Accelerate.framework. Я вважаю, що AMD має подібні функції і в їхній векторній бібліотеці.
Стівен Канон

3

У цій статті показано, як побудувати параболічний алгоритм, який генерує як синус, так і косинус:

DSP трюк: одночасне параболічне наближення гріха і сос

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 для гріха і косинуса, дотримуйтесь шаблону - віднімайте x ^ 2/2! від косинусу відніміть x ^ 3/3! від sine, додайте x ^ 4/4! до косинусу додайте x ^ 5/5! синус ...

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


Насправді коефіцієнт i-синусоїдального коефіцієнта розширення x / i в рази більший від коефіцієнта розширення косинуса. Але я б сумнівався, що використання серіалу Тейлор дійсно швидко ...
Данвіл

1
Чебішев набагато краще, ніж Тейлор, для наближення поліномної функції. Не використовуйте наближення Тейлора.
Timmmm

Тут є купа числових штучних пасів; чисельник і знаменник швидко стають великими, що призводить до помилок з плаваючою комою. Не кажучи вже про те, як ви вирішуєте, що таке «недостатньо точність» і як її обчислити? Наближення Тейлора добре в сусідстві навколо однієї точки; від цього моменту вони швидко стають неточними і вимагають великої кількості термінів, тому пропозиція Тімммма щодо наближення Чебишева (що створює хороші наближення протягом заданого інтервалу) є хорошою.
Jason S

2

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

Пам’ятайте, що cos (x) і sin (x) - це реальна і уявна частини exp (ix). Отже, ми хочемо обчислити exp (ix), щоб отримати обидва. Ми попередньо підраховуємо exp (iy) для деяких дискретних значень y від 0 до 2pi. Зсуваємо х на інтервал [0, 2пі). Тоді вибираємо y, найближчий до x, і пишемо
exp (ix) = exp (iy + (ix-iy)) = exp (iy) exp (i (xy)).

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

Ще одна приємна властивість цього полягає в тому, що ви можете векторизувати його за допомогою SSE.


2

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




0

Ви думали про оголошення таблиць пошуку для двох функцій? Вам все одно доведеться "обчислювати" sin (x) і cos (x), але це було б рівно швидше, якщо вам не потрібен високий ступінь точності.


0

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

 ___libm_sse2_sincos_ (for x86)
 __libm_sse2_sincos_  (for x64)

в оптимізованих складах, якщо вказані відповідні прапорці компілятора (як мінімум / O2 / arch: SSE2 / fp: fast). Назви цих функцій, мабуть, означають, що вони обчислюють не окремий гріх і cos, а обидві "в один крок".

Наприклад:

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

Збірка (для x86) з / fp: швидко:

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: швидко, але з / fp: точно замість цього (що за замовчуванням) викликає окремі 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

So / 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.