Qual é a maneira mais rápida de calcular sin e cos juntos?

100

Gostaria de calcular o seno e o cosseno de um valor juntos (por exemplo, para criar uma matriz de rotação). Claro que eu poderia computá-los separadamente um após o outro a = cos(x); b = sin(x);, mas gostaria de saber se existe uma maneira mais rápida quando precisar dos dois valores.

Edit: Para resumir as respostas até agora:

  • Vlad disse que existe o comando asmFSINCOScomputando os dois (quase ao mesmo tempo que uma chamada paraFSINsozinho)

  • Como Chi notou, esta otimização às vezes já é feita pelo compilador (ao usar sinalizadores de otimização).

  • caf apontou, que funçõessincosesincosfprovavelmente estão disponíveis e podem ser chamadas diretamente apenas incluindomath.h

  • A abordagem de tanascius de usar uma tabela de consulta é discutida como controversa. (No entanto, no meu computador e em um cenário de benchmark, ele é executado 3x mais rápido do quesincoscom quase a mesma precisão para pontos flutuantes de 32 bits.)

  • Joel Goodwin vinculou a uma abordagem interessante de uma técnica de aproximação extremamente rápida com uma precisão muito boa (para mim, isso é ainda mais rápido do que a consulta à tabela)

Danvil
fonte
1
Veja também esta pergunta sobre a implementação nativa de sin / cos: stackoverflow.com/questions/1640595
Joel Goodwin
1
tente sinx ~ x-x^3/6e cosx~1-x^2/4como aproximações se você se preocupa mais com a velocidade do que com a precisão. Você pode adicionar termos em qualquer uma das séries à medida que coloca mais peso na precisão ( en.wikipedia.org/wiki/Taylor_series role para baixo para trig taylor series.) Observe que esta é uma maneira geral de aproximar qualquer função desejada em ntempos diferenciáveis . Portanto, se você tiver alguma função maior à qual os senos e cossenos pertencem, você obterá uma velocidade muito maior se a aproximar em vez de sin, cos independentemente.
Cão
Esta é uma técnica pobre com uma precisão muito pobre. Veja a postagem de Joel Goodwin. A série de Taylor foi postada abaixo. Por favor, poste como uma resposta.
Danvil
1
Bem, depende de seus requisitos, se você quiser precisão, a série de Taylor será uma boa aproximação apenas se você precisar de valores xpróximos a algum ponto x_0, então expanda sua série de Taylor ao redor em x_0vez de 0. Isso lhe dará excelente precisão perto, x_0mas quanto mais longe você pioram os resultados. Você provavelmente pensou que a precisão era péssima quando olhou para a resposta fornecida e tentou valores distantes de 0. Essa resposta é com sin, cos expandido em torno de 0.
ldog

Respostas:

52

Os processadores Intel / AMD modernos possuem instruções FSINCOSpara calcular as funções seno e cosseno simultaneamente. Se você precisa de uma otimização forte, talvez deva usá-la.

Aqui está um pequeno exemplo: http://home.broadpark.no/~alein/fsincos.html

Aqui está outro exemplo (para MSVC): http://www.codeguru.com/forum/showthread.php?t=328669

Aqui está mais um exemplo (com gcc): http://www.allegro.cc/forums/thread/588470

Espero que um deles ajude. (Eu não usei esta instrução, desculpe.)

Como eles são suportados no nível do processador, espero que sejam muito mais rápidos do que as pesquisas de tabela.

Edit:
Wikipedia sugere que FSINCOSfoi adicionado 387 processadores, então você dificilmente pode encontrar um processador que não o suporte.

Edit:
a documentação da Intel afirma que FSINCOSé cerca de 5 vezes mais lento do que FDIV(isto é, divisão de ponto flutuante).

Editar:
Observe que nem todos os compiladores modernos otimizam o cálculo de seno e cosseno em uma chamada para FSINCOS. Em particular, meu VS 2008 não fazia isso.

Edit:
O primeiro link de exemplo está morto, mas aindauma versão na Wayback Machine .

Vlad
fonte
1
@phkahler: Isso seria ótimo. Não sei se essa otimização é usada pelos compiladores modernos.
Vlad,
12
A fsincosinstrução não é "muito rápida". O manual de otimização da própria Intel cita que exige entre 119 e 250 ciclos em micro-arquiteturas recentes. A biblioteca matemática da Intel (distribuída com ICC), por comparação, pode calcular separadamentesin e cosem menos de 100 ciclos, usando uma implementação de software que usa SSE em vez da unidade x87. Uma implementação de software semelhante que calculasse os dois simultaneamente poderia ser ainda mais rápida.
Stephen Canon
2
@Vlad: As bibliotecas matemáticas ICC não são de código aberto e não tenho licença para redistribuí-las, então não posso postar a montagem. Posso dizer que não há sincomputação embutida para eles tirarem vantagem, entretanto; eles usam as mesmas instruções SSE que todos os outros. Para seu segundo comentário, a velocidade relativa a fdivé irrelevante; se houver duas maneiras de fazer algo e uma for duas vezes mais rápida que a outra, não faz sentido chamar a mais lenta de "rápida", independentemente de quanto tempo leva em relação a alguma tarefa completamente não relacionada.
Stephen Canon
1
A sinfunção de software em sua biblioteca oferece precisão total de dupla precisão. A fsincosinstrução oferece um pouco mais de precisão (dupla estendida), mas essa precisão extra é jogada fora na maioria dos programas que chamam a sinfunção, pois seu resultado é geralmente arredondado para precisão dupla por operações aritméticas posteriores ou um armazenamento na memória. Na maioria das situações, eles oferecem a mesma precisão para uso prático.
Stephen Canon
4
Observe também que fsincosnão é uma implementação completa por si só; você precisa de uma etapa de redução de intervalo adicional para colocar o argumento no intervalo de entrada válido para a fsincosinstrução. A biblioteca sine as cosfunções incluem essa redução, bem como a computação principal, de modo que são ainda mais rápidos (em comparação) do que os tempos de ciclo que listei podem indicar.
Stephen Canon
39

Os processadores x86 modernos têm uma instrução fsincos que fará exatamente o que você está pedindo - calcular sen e cos ao mesmo tempo. Um bom compilador de otimização deve detectar o código que calcula sen e cos para o mesmo valor e usar o comando fsincos para executá-lo.

Demorou alguns ajustes de sinalizadores do compilador para que isso funcionasse, mas:

$ 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

Tada, use a instrução fsincos!

Chi
fonte
Isso é legal! Você poderia explicar o que -mfpmath = 387 está fazendo? E também funciona com MSVC?
Danvil
1
Observe isso -ffast-mathe -mfpmathconduza a resultados diferentes em alguns casos.
Debilski
3
mfpmath = 387 forçará o gcc a usar instruções x87 em vez de instruções SSE. Suspeito que o MSVC tenha otimizações e sinalizadores semelhantes, mas não tenho o MSVC em mãos para ter certeza. Usar as instruções do x87 provavelmente prejudicará o desempenho em outro código, você também deve dar uma olhada na minha outra resposta, usar o MKL da Intel.
Chi
Meu antigo gcc 3.4.4 do cygwin produz 2 chamadas separadas para fsine fcos. :-(
Vlad,
Tentei com o Visual Studio 2008 com as maiores otimizações habilitadas. Ele chama 2 funções de biblioteca __CIsine __CIcos.
Vlad,
13

Quando precisar de desempenho, você pode usar uma tabela sin / cos pré-calculada (uma tabela servirá, armazenada como um Dicionário). Bem, depende da precisão que você precisa (talvez a mesa seja muito grande), mas deve ser muito rápido.

tanascius
fonte
Em seguida, o valor de entrada precisa ser mapeado para [0,2 * pi] (ou menor com verificações adicionais) e esta chamada para fmod corrói o desempenho. Em minha implementação (provavelmente abaixo do ideal), não consegui obter desempenho com a tabela de consulta. Você teria algum conselho aqui?
Danvil
11
Uma tabela pré-computada quase certamente será mais lenta do que apenas chamar, sinporque a tabela pré-computada irá destruir o cache.
Andreas Brinck,
1
Depende do tamanho da mesa. Uma tabela de 256 entradas geralmente é bastante precisa e usa apenas 1 KB ... se você a usar muito, ela não ficaria presa no cache sem afetar adversamente o desempenho do restante do aplicativo?
Mr. Boy
@Danvil: Aqui está um exemplo de uma tabela de pesquisa seno en.wikipedia.org/wiki/Lookup_table#Computing_sines . No entanto, presume que você já mapeou sua entrada para [0; 2pi] também.
tanascius
@AndreasBrinck Eu não iria tão longe. Depende (TM). Os caches modernos são enormes e as tabelas de pesquisa são pequenas. Freqüentemente, se você tomar um pouco de cuidado com o layout da memória, sua tabela de pesquisa não precisará fazer nenhuma diferença na utilização do cache do restante de sua computação. O fato de a tabela de pesquisa caber no cache é um dos motivos de sua rapidez. Mesmo em Java, onde é difícil controlar o layout de mem com precisão, tive grandes ganhos de desempenho com tabelas de pesquisa.
Jarrod Smith
13

Tecnicamente, você conseguiria isso usando números complexos e a Fórmula de Euler . Assim, algo como (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();

deve fornecer seno e cosseno em uma única etapa. Como isso é feito internamente é uma questão do compilador e da biblioteca em uso. Pode (e pode) levar mais tempo para fazer isso dessa forma (só porque a Fórmula de Euler é usada principalmente para calcular o complexo expusando sine cos- e não o contrário), mas pode haver alguma otimização teórica possível.


Editar

Os cabeçalhos no <complex>GNU C ++ 4.2 estão usando cálculos explícitos de sine cosdentro polar, então não parece muito bom para otimizações lá, a menos que o compilador faça alguma mágica (veja as opções -ffast-mathe -mfpmathconforme escritas na resposta de Chi ).

Debilski
fonte
desculpe, mas a Fórmula de Euler não diz realmente como calcular algo, é apenas uma identidade (embora muito útil) que relaciona exponenciais complexas a funções trigonométricas reais. Existem benefícios em calcular seno e cosseno juntos, mas eles envolvem subexpressões comuns e sua resposta não discute isso.
Jason S
12

Você pode calcular qualquer um e usar a identidade:

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

mas, como diz @tanascius, uma mesa pré-computada é o caminho a percorrer.

Trigo mitch
fonte
8
E esteja ciente de que usar esse método envolve calcular uma potência e uma raiz quadrada, portanto, se o desempenho for importante, certifique-se de verificar se isso é realmente mais rápido do que calcular a outra função trigonométrica diretamente.
Tyler McHenry,
4
sqrt()é frequentemente otimizado em hardware, por isso pode muito bem ser mais rápido que sin()ou cos(). O poder é apenas auto-multiplicação, então não use pow(). Existem alguns truques para obter raízes quadradas razoavelmente precisas muito rapidamente sem suporte de hardware. Por fim, certifique-se de criar um perfil antes de fazer qualquer um desses.
deft_code
12
Observe que √ (1 - cos ^ 2 x) é menos preciso do que calcular sen x diretamente, em particular quando x ~ 0.
kennytm
1
Para x pequeno, a série de Taylor para y = sqrt (1-x * x) é muito boa. Você pode obter uma boa precisão com os primeiros 3 termos e isso requer apenas algumas multiplicações e um deslocamento. Eu usei em código de ponto fixo.
phkahler de
1
@phkahler: Sua série Taylor não se aplica porque quando x ~ 0, cos x ~ 1.
kennytm
10

Se você usa a biblioteca GNU C, pode fazer:

#define _GNU_SOURCE
#include <math.h>

e você terá declarações dos sincos(), sincosf()e sincosl()funções que calculam os dois valores juntos - presumivelmente no caminho mais rápido para sua arquitetura alvo.

caf
fonte
8

Há coisas muito interessantes nesta página do fórum, que se concentra em encontrar boas aproximações que sejam rápidas: http://www.devmaster.net/forums/showthread.php?t=5784

Aviso: Não usei nada disso sozinho.

Atualização de 22 de fevereiro de 2018: Wayback Machine é a única maneira de visitar a página original agora: https://web.archive.org/web/20130927121234/http://devmaster.net/posts/9648/fast-and-accurate- seno-cosseno

Joel Goodwin
fonte
Eu tentei este também e me deu um desempenho muito bom. Mas sen e cos são calculados independentemente.
Danvil
Minha sensação é que o cálculo de seno / cosseno será mais rápido do que obter seno e usar uma aproximação de raiz quadrada para obter cosseno, mas um teste irá verificar isso. A relação primária entre seno e cosseno é de fase; é possível codificar para que você possa reutilizar os valores de seno que você calculou para as chamadas de cosseno de fase deslocada levando isso em consideração? (Isso pode ser um exagero, mas tinha que perguntar)
Joel Goodwin
Não diretamente (apesar da pergunta exatamente isso). Eu preciso de sin e cos de um valor x e não há como saber se em algum outro lugar eu coincidentemente
calculei
Usei no meu jogo para desenhar um círculo de partículas. Como é apenas um efeito visual, o resultado é próximo o suficiente e o desempenho é realmente impressionante.
Maxim Kamalov
Não estou impressionado; As aproximações de Chebyshev geralmente fornecem a maior precisão para um determinado desempenho.
Jason S
7

Muitas bibliotecas de matemática C, como indica caf, já têm sincos (). A exceção notável é o MSVC.

  • A Sun tem sincos () desde pelo menos 1987 (vinte e três anos; tenho uma página do manual impressa)
  • HPUX 11 tinha em 1997 (mas não está em HPUX 10.20)
  • Adicionado ao glibc na versão 2.1 (fevereiro de 1999)
  • Tornou-se um built-in no gcc 3.4 (2004), __builtin_sincos ().

E com relação à pesquisa, Eric S. Raymond em Art of Unix Programming (2004) (Capítulo 12) diz explicitamente que isso é uma má ideia (no momento presente):

"Outro exemplo é pré-computar pequenas tabelas - por exemplo, uma tabela de sin (x) por grau para otimizar rotações em um mecanismo gráfico 3D ocupará 365 × 4 bytes em uma máquina moderna. Antes que os processadores ficassem mais rápidos do que a memória para exigir o cache , essa era uma otimização de velocidade óbvia. Hoje em dia, pode ser mais rápido recomputar a cada vez, em vez de pagar pela porcentagem de falhas de cache adicionais causadas pela tabela.

"Mas no futuro, isso pode mudar novamente à medida que os caches ficarem maiores. De maneira mais geral, muitas otimizações são temporárias e podem facilmente se transformar em pessimizações conforme as taxas de custo mudam. A única maneira de saber é medir e ver." (da Arte da Programação Unix )

Mas, a julgar pela discussão acima, nem todos concordam.

Joseph Quinsey
fonte
10
"365 x 4 bytes". Você precisa levar em conta os anos bissextos, de modo que na verdade deve ser 365,25 x 4 bytes. Ou talvez ele quisesse usar o número de graus em um círculo em vez do número de dias em um ano terrestre.
Ponkadoodle
@Wallacoloo: Boa observação. Perdi. Mas o erro está no original .
Joseph Quinsey
RI MUITO. Além disso, ele negligencia o fato de que, em muitos dos jogos de computador dessa área, você só precisará de um número finito de ângulos. Não há perdas de cache então, se você conhece os ângulos possíveis. Eu usaria tabelas exatamente neste caso, e daria a fsincos(instrução da CPU!) Uma tentativa para os outros. Freqüentemente, é tão rápido quanto interpolar sen e cos de uma grande mesa.
Erich Schubert
5

Não acredito que as tabelas de pesquisa sejam necessariamente uma boa ideia para esse problema. A menos que seus requisitos de precisão sejam muito baixos, a mesa precisa ser muito grande. E as CPUs modernas podem fazer muitos cálculos enquanto um valor é buscado na memória principal. Esta não é uma daquelas questões que podem ser respondidas adequadamente por argumentos (nem mesmo os meus), teste, meça e considere os dados.

Mas eu observaria as implementações rápidas de SinCos que você encontra em bibliotecas como ACML da AMD e MKL da Intel.

Marca de alto desempenho
fonte
3

Se você deseja usar um produto comercial e está calculando vários cálculos sin / cos ao mesmo tempo (para que possa usar funções vetorizadas), consulte a Biblioteca de Kernel de Matemática da Intel.

Tem uma função sincos

De acordo com essa documentação, a média é de 13,08 relógios / elemento no core 2 duo no modo de alta precisão, o que eu acho que será ainda mais rápido que o fsincos.

Chi
fonte
1
Da mesma forma, no OSX pode-se usar vvsincosou vvsincosfdo Accelerate.framework. Acredito que a AMD também tenha funções semelhantes em sua biblioteca vetorial.
Stephen Canon
2

Quando o desempenho é crítico para esse tipo de coisa, não é incomum introduzir uma tabela de pesquisa.

Tom Cabanski
fonte
2

Para uma abordagem criativa, que tal expandir a série Taylor? Como eles têm termos semelhantes, você poderia fazer algo como o seguinte pseudo:

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
}

Isso significa que você faz algo assim: começando em x e 1 para sen e cosseno, siga o padrão - subtraia x ^ 2/2! do cosseno, subtraia x ^ 3/3! do seno, adicione x ^ 4/4! ao cosseno, adicione x ^ 5/5! para seno ...

Não tenho ideia se isso seria um bom desempenho. Se você precisar de menos precisão do que o sin () e o cos () integrados fornecem, pode ser uma opção.

Tesserex
fonte
Na verdade, o i-fator de extensão do seno é x / i vezes o i-fator de extensão do cosseno. Mas eu duvido que usar a série Taylor seja realmente rápido ...
Danvil
1
Chebyshev é muito melhor do que Taylor para aproximação de função polinomial. Não use a aproximação de Taylor.
Timmmm
Há um monte de gafe numérica aqui; numerador e denominador tornam-se rapidamente grandes e isso leva a erros de ponto flutuante. Sem mencionar como você decide o que é "precisão insuficiente" e como calculá-la? A aproximação de Taylor é boa na vizinhança em torno de um único ponto; a partir desse ponto, eles rapidamente se tornam imprecisos e exigem um grande número de termos, razão pela qual a sugestão de Timmmm sobre a aproximação de Chebyshev (que cria boas aproximações em um determinado intervalo) é boa.
Jason S
2

Há uma boa solução na biblioteca CEPHES que pode ser bem rápida e você pode adicionar / remover precisão de forma bastante flexível por um pouco mais / menos tempo de CPU.

Lembre-se de que cos (x) e sin (x) são as partes reais e imaginárias de exp (ix). Portanto, queremos calcular exp (ix) para obter ambos. Pré-calculamos exp (iy) para alguns valores discretos de y entre 0 e 2pi. Mudamos x para o intervalo [0, 2pi). Em seguida, selecionamos y que está mais próximo de x e escrevemos
exp (ix) = exp (iy + (ix-iy)) = exp (iy) exp (i (xy)).

Obtemos exp (iy) da tabela de pesquisa. E desde | xy | for pequeno (no máximo metade da distância entre os valores de y), a série de Taylor convergirá bem em apenas alguns termos, então usamos isso para exp (i (xy)). E então precisamos apenas de uma multiplicação complexa para obter exp (ix).

Outra propriedade interessante disso é que você pode vetorizá-lo usando SSE.

Jsl
fonte
2

Você pode querer dar uma olhada em http://gruntthepeon.free.fr/ssemath/ , que oferece uma implementação vetorizada SSE inspirada na biblioteca CEPHES. Tem boa precisão (desvio máximo de sin / cos na ordem de 5e-8) e velocidade (supera ligeiramente fsincos em uma base de chamada única e um vencedor claro sobre vários valores).

SleuthEye
fonte
1

Eu postei uma solução envolvendo montagem ARM em linha capaz de calcular o seno e o cosseno de dois ângulos de uma vez aqui: Fast seno / cosseno para ARMv7 + NEON

jcayzac
fonte
1

Uma aproximação precisa, porém rápida, da função sin e cos simultaneamente, em javascript, pode ser encontrada aqui: http://danisraelmalta.github.io/Fmath/ (facilmente importado para c / c ++)

user2781980
fonte
0

Você já pensou em declarar tabelas de pesquisa para as duas funções? Você ainda teria que "calcular" sin (x) e cos (x), mas seria decididamente mais rápido, se você não precisar de um alto grau de precisão.

Frank Shearar
fonte
0

O compilador MSVC pode usar as funções SSE2 (internas)

 ___libm_sse2_sincos_ (for x86)
 __libm_sse2_sincos_  (for x64)

em compilações otimizadas se os sinalizadores de compilador apropriados forem especificados (no mínimo / O2 / arch: SSE2 / fp: rápido). Os nomes dessas funções parecem implicar que elas não calculam sen e cos separados, mas ambos "em uma única etapa".

Por exemplo:

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

Montagem (para x86) com / fp: rápido:

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

Montagem (para x86) sem / fp: rápido, mas com / fp: preciso em vez (que é o padrão) chama sen e cos separados:

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

Portanto, / fp: fast é obrigatório para a otimização do sincos.

Mas observe que

___libm_sse2_sincos_

talvez não seja tão preciso quanto

__libm_sse2_sin_precise
__libm_sse2_cos_precise

devido à falta de "preciso" no final de seu nome.

No meu sistema "ligeiramente" mais antigo (Intel Core 2 Duo E6750) com o compilador MSVC 2019 mais recente e otimizações apropriadas, meu benchmark mostra que a chamada sincos é cerca de 2,4 vezes mais rápida do que chamadas sin e cos separadas.

xy
fonte