- CORDIC é um algoritmo que transforma cálculos complexos em operações centradas em adição e deslocamento de bits para calcular funções trigonométricas como
sin, cos e tan sem FPU nem grandes tabelas de consulta
- Esse método é mais útil em ambientes embarcados do que em sistemas de alto desempenho, especialmente em microcontroladores de baixo poder e FPGAs, e seu valor não pode ser julgado apenas pela velocidade
- Usando ponto fixo em vez de ponto flutuante, é possível dividir os 16 bits superiores de um
int32_t como parte inteira e os 16 inferiores como parte fracionária, representando aproximadamente de -32768.99997 até 32767.99997
- Ao rotacionar um vetor com ângulos cada vez menores até o ângulo-alvo e usar 16 valores em uma tabela de
atan(2**-i) junto com o valor inicial x=39796, é possível substituir a multiplicação por deslocamentos de bits em cada iteração
- Após 16 iterações no ângulo de exemplo
0.9152, o erro absoluto de sin(0.9152) cai para 0.00000956 e o de cos(0.9152) para 0.0000434
Ambientes de cálculo em que o CORDIC se encaixa bem
- CORDIC é um algoritmo para calcular funções trigonométricas como
sin, cos e tan em hardware de baixo consumo
- Ele funciona mesmo em ambientes sem FPU, isto é, sem unidade de ponto flutuante, ou onde é difícil usar grandes tabelas de consulta
- O cálculo real é composto principalmente por adições e deslocamentos de bits simples
- Ele combina matemática vetorial, trigonometria, convergência e ideias da ciência da computação para aproximar funções complexas com operações simples
- Em hardware de alto desempenho, essa técnica pode não ser indispensável
- Seu principal alvo de aplicação são ambientes embarcados
- Em especial, ele é adequado para microcontroladores de baixo desempenho e FPGAs
- Pode haver hardware ou periféricos mais rápidos, mas velocidade não é a única medida de utilidade
Representação em ponto fixo para evitar ponto flutuante
- Mesmo funções como
sin(x), que produzem valores entre -1.0 e 1.0, não precisam necessariamente ser representadas com ponto flutuante
- Ponto fixo representa números racionais fixando a posição da vírgula dentro de um tipo inteiro
- No exemplo, um
int32_t é dividido em 16 bits superiores para a parte inteira e 16 bits inferiores para a parte fracionária
- Nesse caso, o intervalo é aproximadamente de
-32768.99997 até 32767.99997
- Dependendo de onde a vírgula é colocada, há uma troca entre alcance da parte inteira e precisão da parte fracionária
- O valor em si continua sendo um
int32_t, e o programador atribui significado extra ao arranjo de bits
Conversão para ponto fixo e operações básicas
- Se a precisão fracionária for de 16 bits, um valor float como
42.01 pode ser transformado em ponto fixo multiplicando por (1 << 16)
42.01 * (1 << 16), ao fazer cast para int32_t, vira 2753167
- Para voltar a float, basta calcular
2753167 / (1 << 16) e obter aproximadamente 42.0099945
- Também é possível codificar diretamente um valor como
1.5 sem usar ponto flutuante
- A parte inteira
1 é elevada com (1 << 16)
- Metade da parte fracionária pode ser colocada em
0x7fff, o valor intermediário entre 0x0000 e 0xffff
- O resultado dessa forma é o decimal
98303
- Entre valores que usam o mesmo fator de escala, adição e subtração funcionam diretamente
- Na multiplicação, multiplica-se os dois valores em ponto fixo e depois desloca-se o resultado para a direita pelo fator de escala
- Na divisão, é possível obter precisão extra deslocando antes o dividendo para a esquerda pelo fator de escala e então dividindo pelo divisor
Aproximando funções trigonométricas com rotação de vetores
- CORDIC é a sigla de “co-ordinate rotation digital computer” e foi criado em meados da década de 1950
- A ideia central é rotacionar um vetor sobre o círculo unitário com ângulos cada vez menores, de modo que, ao atingir o ângulo-alvo, os componentes do vetor se tornem os valores de seno e cosseno
- Esse processo avança de maneira parecida com uma busca binária
- Move-se em direção ao ângulo-alvo com um ângulo grande
- Verifica-se se o alvo foi ultrapassado
- Depois repete-se a rotação no sentido horário ou anti-horário com ângulos menores
- Como exemplo, ao calcular
sin(0.7), começa-se com o vetor inicial (1, 0) e com o alvo em 0.7 radianos
- Primeiro faz-se uma rotação anti-horária de
0.7853 radianos, isto é, 45˚
- O alvo restante passa a ser
0.7 - 0.7853 = -0.0853
- Como o valor é negativo, a próxima rotação é horária em
0.3926 radianos, isto é, 22.5˚
- Depois, conforme o sinal do alvo restante, a direção vai sendo invertida com ângulos menores como
0.1963 radianos
- Após 16 iterações, o vetor quase coincide com o ângulo original desejado, e
y se torna uma aproximação de sin(a), enquanto x aproxima cos(a)
Reduzindo operações caras na matriz de rotação
- Uma rotação de vetor comum usa multiplicação de matrizes com seno e cosseno
- O CORDIC usa identidades trigonométricas para reescrever a matriz de rotação em torno de
tan(a)
- Como no início são usados ângulos de rotação fixos como
45˚, 22.5˚ e 11.25˚, os valores de tan(a) podem ser colocados em uma tabela pré-calculada
- Essa tabela precisa de apenas 16 valores
uint32_t, totalizando 64 bytes
- Como comparação, uma tabela
sin(x) não otimizada com 4096 valores entre -1 e 1 exigiria 16KiB e ainda teria baixa precisão
- O termo
cos(a) na frente aparece em cada rotação, mas o produto de todos eles converge para uma constante
- Com ângulos como
45˚, 22.5˚ e 11.25˚, esse produto é aproximadamente 0.6366
- Basta multiplicar por essa constante uma única vez ao fim de todas as iterações
Escolha dos ângulos para sobrar apenas deslocamentos e adições
- Para eliminar multiplicações, escolhem-se ângulos cujo resultado de
tan(a) seja sempre uma potência inversa de 2
- Para isso, cria-se uma tabela com 16 entradas contendo
atan(2**-i) para cada iteração i=0 até 15
- Os ângulos reais de rotação passam a ser
45˚, 26.565˚, 14.036˚, 7.125˚ e assim por diante
- Embora os ângulos não diminuam exatamente pela metade, o processo ainda converge corretamente usando esses valores
- A multiplicação por
tan(a) vira um deslocamento de bits pelo número de iteração i
- O produto dos termos
cos(a) também é recalculado de acordo com essa nova escolha de ângulos
- O valor é aproximadamente
0.60725
- Em ponto fixo de 16 bits, isso vira
39796
- Em vez de multiplicar no final, basta iniciar o vetor com
x igual a 39796 em vez de 1
Procedimento do algoritmo
- Na etapa de pré-cálculo, monta-se uma tabela em que cada entrada é
atan(2**-i) e cada valor é convertido para ponto fixo
- A fórmula de conversão é
atan(2**-i) * (1 << 16)
- Ao calcular
sin ou cos, o ângulo de entrada também é convertido para ponto fixo
- O exemplo
0.9152 vira 0.9152 * (1 << 16) = 59978
- O estado inicial é o seguinte
x = 39796
y = 0
z = 59978
z não faz parte do vetor; ele é o valor usado para acompanhar o ângulo-alvo restante
- O sinal de
z determina a direção da rotação
- Se
z >= 0, a rotação é anti-horária e executa-se z -= table[i]
- Se
z < 0, a rotação é horária e executa-se z += table[i]
- Em cada iteração,
x e y usam apenas adição, subtração e deslocamento >> i
if z >= 0:
x_next = x - (y >> i)
y_next = y + (x >> i)
z -= table[i]
else:
x_next = x + (y >> i)
y_next = y - (x >> i)
z += table[i]
x = x_next
y = y_next
Resultado de convergência do exemplo e temas restantes
- No exemplo de
0.9152 radianos, na primeira iteração z é positivo, então o vetor gira no sentido anti-horário cerca de 0.785 radianos
- Na segunda iteração,
z ainda é positivo, então ele gira cerca de 0.436 radianos no sentido anti-horário, mas passa do alvo
- Na terceira iteração,
z se torna negativo e o vetor gira cerca de 0.244 radianos no sentido horário
- Na quarta iteração,
z continua negativo, então ele gira cerca de 0.124 radianos no sentido horário
- À medida que a variação angular diminui, o vetor oscila para frente e para trás cada vez mais perto do resultado real até convergir
- Após 16 iterações,
y se torna uma aproximação muito próxima de sin(0.9152)
- O erro absoluto do seno é
0.00000956
- O erro absoluto do cosseno em
x é 0.0000434
- Ainda há tópicos que não foram abordados
- O tratamento especial necessário quando o ângulo de interesse está fora do primeiro ou do quarto quadrante do círculo unitário
- Variações do CORDIC capazes de calcular
tan, atan, asin, acos, sinh, cosh, tanh, sqrt, ln e e^x
- O algoritmo relacionado BKM, projetado para cálculo de logaritmos e exponenciais
- Há planos de tratar esses assuntos com mais detalhes no canal do YouTube Low Byte Productions
1 comentários
Opiniões do Hacker News
O autor disse que isso se aplica principalmente a lugares como FPGA, mas também pode ser usado em desenvolvimento de jogos ou simulações físicas distribuídas
Cálculos de ponto flutuante tornam complicado alinhar a determinismo entre plataformas, e uma solução é evitar totalmente ponto flutuante e implementar um motor de física de ponto fixo
Para implementar funções trigonométricas, é preciso algo como CORDIC
Alguns anos atrás, comecei a fazer algo assim por diversão, mas não terminei, e gostaria de tentar de novo algum dia
https://randomascii.wordpress.com/2013/07/16/floating-point-...
Em resumo, o x87 tinha suas esquisitices; é preciso manter consistentes configurações como modo de arredondamento e flush-to-zero; processadores antigos não têm FMA; instruções aproximadas como
mmsqrtpsnão têm uma especificação consistente; e o compilador pode reassociar expressõesEm uma rotina pequena ou em uma biblioteca escrita por você, embora seja doloroso, é possível garantir que essas coisas sejam evitadas
O IEEE-754 2008 deixou a especificação mais clara e, na prática, partiu do pressuposto da morte do x87; em 2024, dá para evitar o x87 com segurança
FMA também faz parte da especificação IEEE-754 2008 e está presente em processadores modernos, incluindo os posteriores ao Intel Haswell
Ainda assim, diferenças de arquitetura como AVX2 de 8 lanes e NEON de 4 lanes podem atrapalhar, mas, usando assembly, intrínsecos ou C verificado com Compiler Explorer ou objdump, dá para olhar a saída e julgar: “isso será consistente”
“Na verdade, antes de o IEEE 754 se tornar um padrão tão popular quanto é hoje, ponto fixo era usado o tempo todo. Pergunte a um desenvolvedor de jogos que trabalhou entre 1980 e por volta de 2000, e ele contará os detalhes”
A nova biblioteca Rapier, reescrita a partir da nphysics, em vez disso se apoia nas garantias do IEEE-754 2008 para oferecer determinismo entre plataformas
Por isso, não funciona em plataformas antigas, mas é determinística em plataformas modernas, incluindo wasm
É claro que não dá para depender das rotinas de funções transcendentais como sin e cos fornecidas por cada plataforma; é preciso implementá-las diretamente para que se comportem da mesma forma em todos os lugares
Mas, se não for executada em plataformas não conformes, é uma abordagem viável
https://www.rustsim.org/blog/2020/06/01/this-month-in-rustsi...
https://rapier.rs/docs/user_guides/rust/determinism/
CORDIC pode ser usado não só para calcular e gerar seno e cosseno, mas também para várias operações como logaritmos, exponenciais, raízes quadradas, magnitude de vetores, conversão entre coordenadas polares e cartesianas e rotação de vetores
O autor também antecipa essas possibilidades na conclusão
Tenho a impressão de que, usando quatérnios em vez das matrizes ortonormais convencionais, operações baseadas em CORDIC poderiam ser executadas de forma mais eficiente, isto é, usando menos ciclos de computação e menos memória, além de reduzir erros
https://core.ac.uk/works/8439118
Aprendi séries de Taylor no pré-cálculo do ensino médio, e o professor disse que as funções trigonométricas das calculadoras eram implementadas assim na prática
Quando fui pesquisar, vi que na verdade era CORDIC, e me diverti implementando em TI Basic
Não era CORDIC, mas há semelhanças no algoritmo
http://files.righto.com/calculator/sinclair_scientific_simul...
Textos sobre implementação em hardware:
https://arxiv.org/pdf/2211.04053
https://hal.science/hal-01327460/document
https://archive.ll.mit.edu/HPEC/agendas/proc05/Day_1/Abstrac...
Gostaria de ver como isso se compara, em diferentes hardwares ao longo do tempo, às implementações de funções trigonométricas comuns em software e hardware
Com o crescimento de IoT e da comunicação máquina a máquina, e considerando as implementações de CORDIC e sua eficiência computacional, seu uso provavelmente deve aumentar bastante; portanto, são necessárias boas referências para implementações corretas e otimizadas
Como exceção, há os livros do Prof. Omondi e do Prof. Deschamps
https://www.worldscientific.com/worldscibooks/10.1142/p1054
http://www.arithmetic-circuits.org/guide2fpga/vhdl_codes.htm
sin e cos são usados com frequência em rotações de vetores
Nesse caso, o truque do CORDIC é evitar o cálculo tradicional de sin/cos/multiplicação e passar o próprio vetor a ser rotacionado como entrada do CORDIC
Assim, o CORDIC produz diretamente o vetor rotacionado sem calcular sin/cos nem fazer multiplicação de números complexos
O CORDIC brilha especialmente quando a latência não é tão importante
Se cada etapa do cálculo for colocada em pipeline, é possível obter uma vazão alta, o que combina bem com mixagem digital em sistemas sem fio
Em 2023, alguns MCUs modernos são baratos e ainda assim têm FPU
O STM32G4 é um bom exemplo e, diferentemente de casos como MCUs M0, se você não quiser usar ponto fixo, pode usar
f32à vontadeEsses chips podem ser encontrados por cerca de 1 a 2 dólares por MCU
Ainda assim, o G4 também tem um periférico CORDIC em hardware que implementa esse algoritmo para uso com ponto fixo
Fico curioso se isso serve principalmente para evitar perdas de precisão de ponto flutuante
Ele é programado por registradores, mas não é uma implementação direta do CORDIC na CPU; um hardware dedicado dentro do IC é que faz o processamento
O STM32G4 mais barato é o STM32G441KBT6 e, arredondando, custa 4 dólares https://www.digikey.com/en/products/detail/microchip-technol...
Fico curioso para saber onde se encontra por menos de 2 dólares
Na Digi-Key, o chip da Nuvoton mal fica abaixo de 2 dólares na quantidade de 500 unidades
Ele é rápido e lida com produtos intermediários de 64 bits, então a precisão de divisão e de funções trigonométricas é suficiente para a maioria dos usos
Se necessário, também dá para aumentar ainda mais a precisão por software
Conheci o CORDIC tarde; antes disso, no mundo do assembly de 8 e 16 bits, eu usava muito ponto fixo por desempenho e determinismo
Quando descobri, fiquei surpreso
Era rápido, e a matemática necessária para usá-lo de forma útil era apenas a básica
Isso me lembrou um trecho de código bem bonitinho em que participei no passado
Era preciso encontrar as coordenadas da bissetriz do ângulo formado por um arco no círculo unitário, e as coordenadas
(x,y)dos dois braços já estavam disponíveisA implementação existente era um amontoado de trigonometria: convertia as coordenadas
(x,y)para coordenadas polares(r,θ), verificava se oθcalculado estava no quadrante correto, dividiaθpela metade e então convertia de volta para(x,y)No fim, chamava muitas funções trigonométricas e inversas
Como era Python e números complexos podiam ser usados como cidadãos de primeira classe, bastou definir dois números complexos,
z1a partir de(x1,y1)ez2a partir de(x2,y2), e tirar apenas a média geométrica do produto√(z1*z2)No código novo não havia funções trigonométricas explícitas, nem conversões e conversões inversas explícitas
https://fgiesen.wordpress.com/2010/10/21/finish-your-derivat...
Está escrito: “é bastante óbvio que girar 22,75˚ é o mesmo que girar 45˚ e depois -22,5˚”; então não seria uma rotação de 22,5°?
Fico na dúvida se é um erro do texto ou se eu entendi errado
O sistema de octree de Meagher é famoso por usar apenas aritmética inteira, sem multiplicação nem divisão de inteiros
“Foram desenvolvidos algoritmos eficientes de tempo linear para exibição, incluindo operações booleanas (união, interseção, diferença), operações geométricas (translação, redimensionamento, rotação), detecção de interferência em N dimensões e remoção de superfícies ocultas em pontos arbitrários do espaço. Esses algoritmos não requerem operações de ponto flutuante, multiplicação de inteiros nem divisão de inteiros”
https://doi.org/10.1016/0146-664X(82)90104-6
Graças a isso, ficou mais fácil criar hardware acelerador gráfico VLSI rápido e personalizado para representações em octree
Fico curioso sobre o desempenho do CORDIC em comparação com interpolação cúbica usando uma tabela pequena, ou com outras interpolações polinomiais
Aprendi que sintetizadores com recursos limitados às vezes usavam interpolação cúbica; provavelmente era uma época em que o CORDIC ainda era relativamente novo
Olhando por alto, como o CORDIC ganha 1 bit de precisão a cada iteração, o cálculo deve ser mais caro, mas deve usar menos espaço do que polinômios
Porém, do ponto de vista de espaço, é importante destacar que ele pode sair mais barato do que a tabela de consulta com 4096 itens para
sin(x)apresentada no textoGraças à simetria, só é necessário 1/4 do círculo inteiro
Usar ângulos do tamanho de um byte era conveniente porque eles circulavam automaticamente, e
2^8era bastante suficiente para rotações em jogos 2DMas, se você quiser movimentos suaves, em 3D isso não vai muito longe