3 points par GN⁺ 2024-05-12 | 1 commentaires | Partager sur WhatsApp
  • CORDIC est un algorithme qui transforme des calculs complexes en opérations principalement fondées sur des additions et des décalages de bits, afin de calculer des fonctions trigonométriques comme sin, cos et tan sans FPU ni grande table de correspondance
  • Cette approche est utile dans les environnements embarqués, en particulier sur les microcontrôleurs peu performants et les FPGA, plutôt que dans les systèmes haute performance ; il est difficile d’en juger la valeur à la seule vitesse
  • En utilisant la virgule fixe plutôt que la virgule flottante, on peut diviser un int32_t en 16 bits de poids fort pour la partie entière et 16 bits de poids faible pour la partie fractionnaire, afin de représenter environ de -32768.99997 à 32767.99997
  • En faisant tourner un vecteur vers un angle cible par angles de plus en plus petits, avec une table de 16 valeurs atan(2**-i) et une valeur initiale x=39796, on peut remplacer les multiplications à chaque itération par des décalages de bits
  • Pour l’angle d’exemple 0.9152, après 16 itérations, l’erreur absolue de sin(0.9152) descend à environ 0.00000956, et celle de cos(0.9152) à environ 0.0000434

Les environnements de calcul où CORDIC est adapté

  • CORDIC est un algorithme destiné à calculer des fonctions trigonométriques comme sin, cos et tan sur du matériel basse consommation
  • Il fonctionne même dans des environnements dépourvus de FPU, c’est-à-dire d’unité de calcul en virgule flottante, ou lorsqu’il est difficile d’utiliser de grandes tables de correspondance
  • Les calculs effectifs reposent surtout sur de simples additions et décalages de bits
  • Il combine des idées de calcul vectoriel, de trigonométrie, de convergence et d’informatique pour approximer des fonctions complexes avec des opérations simples
  • Sur du matériel haute performance, cette technique n’est pas forcément nécessaire
    • Sa principale cible est l’embarqué
    • Elle convient particulièrement aux microcontrôleurs peu performants et aux FPGA
    • Il peut exister du matériel ou des périphériques plus rapides, mais la vitesse n’est pas le seul critère d’utilité

Représentation en virgule fixe pour éviter la virgule flottante

  • Même les fonctions qui renvoient des valeurs entre -1.0 et 1.0, comme sin(x), n’ont pas nécessairement besoin d’être représentées en virgule flottante
  • La virgule fixe représente des nombres rationnels à l’intérieur d’un type entier en fixant la position de la virgule
  • L’exemple divise un int32_t en 16 bits de poids fort pour la partie entière et 16 bits de poids faible pour la partie fractionnaire
    • Dans ce cas, la plage va approximativement de -32768.99997 à 32767.99997
    • Selon l’endroit où l’on place la virgule, on peut arbitrer entre l’étendue de la partie entière et la précision de la partie fractionnaire
  • La valeur elle-même reste un int32_t, auquel le programmeur attribue une signification supplémentaire dans l’agencement des bits

Conversion en virgule fixe et opérations de base

  • Si la précision fractionnaire est de 16 bits, on peut convertir une valeur float comme 42.01 en virgule fixe en la multipliant par (1 << 16)
    • 42.01 * (1 << 16), converti en int32_t, donne 2753167
    • Pour revenir en float, on calcule 2753167 / (1 << 16), ce qui donne environ 42.0099945
  • On peut aussi encoder directement des valeurs comme 1.5 sans utiliser du tout la virgule flottante
    • La partie entière 1 est portée par (1 << 16)
    • La moitié fractionnaire peut être placée à 0x7fff, la valeur médiane entre 0x0000 et 0xffff
    • Le résultat de cette méthode est 98303 en décimal
  • Entre des valeurs utilisant le même facteur d’échelle, l’addition et la soustraction fonctionnent telles quelles
  • Pour la multiplication, on multiplie deux valeurs en virgule fixe, puis on décale de nouveau le résultat vers la droite du facteur d’échelle
  • Pour la division, on peut obtenir une précision supplémentaire en décalant d’abord le dividende vers la gauche du facteur d’échelle, puis en le divisant par le diviseur

Approximer les fonctions trigonométriques par rotation de vecteur

  • CORDIC est l’abréviation de « co-ordinate rotation digital computer » et a été créé au milieu des années 1950
  • L’idée centrale consiste à faire tourner un vecteur sur le cercle unité par angles de plus en plus petits, afin que, lorsqu’il atteint l’angle cible, ses composantes deviennent les valeurs du sinus et du cosinus
  • Le processus ressemble à une recherche binaire
    • On se déplace vers l’angle cible avec un grand angle
    • On vérifie si l’on a dépassé la cible
    • Puis on répète des rotations dans le sens horaire ou antihoraire avec des angles plus petits
  • Pour calculer sin(0.7), par exemple, on part du vecteur initial (1, 0) et de la cible 0.7 radian
    • On commence par une rotation antihoraire de 0.7853 radian, soit 45˚
    • La cible restante devient 0.7 - 0.7853 = -0.0853
    • Comme la valeur est négative, on effectue ensuite une rotation horaire de 0.3926 radian, soit 22.5˚
    • Ensuite, selon le signe de la cible restante, on change de direction avec des angles plus petits, comme 0.1963 radian
  • Après 16 itérations, le vecteur est presque aligné sur l’angle cible initial, y donne une approximation de sin(a) et x une approximation de cos(a)

Réduire les opérations coûteuses dans la matrice de rotation

  • Une rotation vectorielle classique utilise une multiplication matricielle contenant des sinus et des cosinus
  • CORDIC utilise des identités trigonométriques pour reformuler la matrice de rotation autour de tan(a)
  • Au départ, comme on utilise des angles de rotation fixes tels que 45˚, 22.5˚ et 11.25˚, les valeurs de tan(a) peuvent être placées dans une table précalculée
  • Cette table ne nécessite que 16 uint32_t, soit 64 octets
    • À titre de comparaison, une table sin(x) non optimisée contenant 4096 valeurs de -1 à 1 nécessiterait 16 KiB, avec une précision considérée comme faible
  • Le terme cos(a) qui précède chaque rotation apparaît à chaque itération, mais le produit de tous ces termes converge vers une constante
    • Avec des angles du type 45˚, 22.5˚, 11.25˚, ce produit vaut environ 0.6366
    • Il suffit de multiplier par cette constante une seule fois après toutes les itérations

Choisir les angles pour ne garder que décalages et additions

  • Pour éliminer les multiplications, on choisit les angles de sorte que le résultat de tan(a) soit toujours une puissance inverse de 2
  • Pour cela, on crée une table de 16 entrées contenant, pour chaque itération de i=0 à 15, la valeur atan(2**-i)
  • Les angles de rotation effectifs deviennent 45˚, 26.565˚, 14.036˚, 7.125˚, etc.
  • Les angles ne diminuent pas exactement de moitié, mais le processus converge tout de même vers le bon résultat avec ces angles
  • La multiplication par tan(a) est remplacée par un décalage de bits correspondant au numéro d’itération i
  • Le produit des termes cos(a) est lui aussi recalculé en fonction de ce nouveau choix d’angles
    • Sa valeur est d’environ 0.60725
    • En virgule fixe sur 16 bits, cela donne 39796
    • Au lieu de multiplier par cette valeur à la fin, on peut initialiser le x du vecteur à 39796 plutôt qu’à 1

Procédure de l’algorithme

  • Lors de l’étape de précalcul, on crée une table dont chaque entrée vaut atan(2**-i), puis on convertit chaque valeur en virgule fixe
    • La formule de conversion est atan(2**-i) * (1 << 16)
  • Pour calculer sin ou cos, on convertit aussi l’angle d’entrée en virgule fixe
    • L’exemple 0.9152 devient 0.9152 * (1 << 16) = 59978
  • L’état initial est le suivant
x = 39796
y = 0
z = 59978
  • z ne fait pas partie du vecteur : il sert à suivre l’angle cible restant
  • Le signe de z détermine le sens de rotation
    • Si z >= 0, on effectue une rotation antihoraire et on applique z -= table[i]
    • Si z < 0, on effectue une rotation horaire et on applique z += table[i]
  • Chaque itération n’utilise que des additions, des soustractions et des décalages >> i sur x et y
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

Exemple de convergence et sujets restants

  • Dans l’exemple à 0.9152 radian, z est positif à la première itération, donc on effectue une rotation antihoraire d’environ 0.785 radian
  • À la deuxième itération, z est encore positif, donc on effectue une rotation antihoraire d’environ 0.436 radian, mais on dépasse la cible
  • À la troisième itération, z devient négatif, et on effectue une rotation horaire d’environ 0.244 radian
  • À la quatrième itération aussi, z est négatif, donc on effectue une rotation horaire d’environ 0.124 radian
  • À mesure que les variations d’angle diminuent, le vecteur oscille autour du résultat réel et converge
  • Après 16 itérations, y est une approximation très proche de sin(0.9152)
    • L’erreur absolue sur le sinus est de 0.00000956
    • L’erreur absolue sur le cosinus donné par x est de 0.0000434
  • Certains sujets restent à traiter
    • Le traitement spécial nécessaire lorsque l’angle d’intérêt se trouve en dehors des 1er ou 4e quadrants du cercle unité
    • Les variantes de CORDIC permettant de calculer tan, atan, asin, acos, sinh, cosh, tanh, sqrt, ln, e^x
    • L’algorithme apparenté BKM, conçu pour le calcul des logarithmes et des exponentielles
  • Ces sujets seront traités plus en détail sur la chaîne YouTube Low Byte Productions

1 commentaires

 
GN⁺ 2024-05-12
Avis de Hacker News
  • L’auteur dit que cela s’applique surtout à des environnements comme les FPGA, mais on peut aussi l’utiliser dans le développement de jeux ou les simulations physiques distribuées
    Les calculs en virgule flottante rendent la déterminisme entre plateformes difficile à garantir, et une solution consiste à éviter complètement la virgule flottante pour implémenter un moteur physique en virgule fixe
    Pour implémenter les fonctions trigonométriques, il faut quelque chose comme CORDIC
    Il y a quelques années, j’avais commencé à construire ce genre de chose pour m’amuser, mais je ne l’ai jamais terminé, et j’aimerais m’y remettre un jour
    https://randomascii.wordpress.com/2013/07/16/floating-point-...

    • Cet article a maintenant 10 ans, mais il contient une citation importante : « La norme IEEE garantit certaines choses. Elle en garantit plus que ne le savent ceux qui voient les maths en virgule flottante comme de la mystique, mais moins que ne le pensent certains programmeurs »
      En résumé, x87 avait ses bizarreries ; il faut aligner de manière cohérente des réglages comme le mode d’arrondi et le flush-to-zero ; les anciens processeurs n’ont pas de FMA ; les instructions d’approximation comme mmsqrtps n’ont pas de spécification cohérente ; et les compilateurs peuvent réassocier les expressions
      Pour de petites routines ou une bibliothèque écrite à la main, il est possible, même si c’est pénible, de garantir qu’on évite ces problèmes
      IEEE-754 2008 a clarifié la spécification et supposait en pratique la mort de x87 ; en 2024, on peut clairement éviter x87
      Le FMA fait aussi partie de la spécification IEEE-754 2008 et se trouve dans les processeurs modernes, y compris depuis Intel Haswell
      Malgré tout, des différences d’architecture comme AVX2 en 8-wide et NEON en 4-wide peuvent poser problème, mais avec de l’assembleur, des intrinsèques, ou du C vérifié dans Compiler Explorer ou avec objdump, on peut examiner la sortie et se dire : « ceci sera cohérent »
    • L’auteur a aussi dit qu’avant que la virgule flottante ne se généralise grâce aux progrès des performances matérielles, la virgule fixe était très courante dans le développement de jeux, et que CORDIC était probablement utilisé avec
      « En fait, avant qu’IEEE 754 ne devienne un standard aussi populaire qu’aujourd’hui, la virgule fixe était utilisée partout. Demandez à un développeur de jeux ayant travaillé entre 1980 et environ 2000, il vous en parlera en détail »
    • La bibliothèque de simulation physique pour le développement de jeux nphysics avait choisi d’utiliser des maths en virgule fixe avec CORDIC lorsqu’un déterminisme entre plateformes était nécessaire, mais elle est désormais abandonnée
      Rapier, la nouvelle bibliothèque réécrite à partir de nphysics, s’appuie plutôt sur les garanties d’IEEE-754 2008 pour fournir un déterminisme entre plateformes
      Elle ne fonctionne donc pas sur les anciennes plateformes, mais elle est déterministe sur les plateformes modernes, y compris wasm
      Bien sûr, on ne peut pas dépendre des routines de fonctions transcendantes comme sin et cos fournies par chaque plateforme ; il faut les implémenter soi-même pour qu’elles se comportent partout de la même manière
      Mais si l’on n’exécute pas sur des plateformes non conformes, c’est une approche possible
      https://www.rustsim.org/blog/2020/06/01/this-month-in-rustsi...
      https://rapier.rs/docs/user_guides/rust/determinism/
  • CORDIC peut servir non seulement à calculer et générer sinus et cosinus, mais aussi à de nombreuses opérations comme les logarithmes, exponentielles, racines carrées, normes de vecteurs, conversions coordonnées polaires-cartésiennes et rotations de vecteurs
    L’auteur évoque aussi ces possibilités dans la conclusion
    J’ai l’impression qu’en utilisant des quaternions au lieu des matrices orthonormées habituelles, les opérations basées sur CORDIC pourraient être exécutées plus efficacement, c’est-à-dire avec moins de cycles de calcul et de mémoire, et avec moins d’erreurs
    https://core.ac.uk/works/8439118

    • Si ma mémoire est bonne, on peut aussi l’étendre à des groupes de Lie arbitraires
  • En pré-calcul au lycée, j’ai appris les séries de Taylor, et mon professeur nous a dit que les fonctions trigonométriques des calculatrices étaient réellement implémentées comme ça
    En cherchant, j’ai découvert qu’en réalité c’était CORDIC, et je me suis amusé à l’implémenter en TI Basic

    • Il serait intéressant de lire comment l’étonnante calculatrice Sinclair Scientific calculait les fonctions trigonométriques, les logarithmes, etc.
      Ce n’était pas CORDIC, mais l’algorithme présente des similarités
      http://files.righto.com/calculator/sinclair_scientific_simul...
    • Existe-t-il vraiment au moins une calculatrice qui utilise les développements de Taylor ?
  • Articles sur les implémentations matérielles :
    https://arxiv.org/pdf/2211.04053
    https://hal.science/hal-01327460/document
    https://archive.ll.mit.edu/HPEC/agendas/proc05/Day_1/Abstrac...
    J’aimerais voir comment cela se compare aux implémentations de fonctions trigonométriques logicielles et matérielles classiques sur divers matériels selon les époques.

    • Il est étrange que CORDIC, bien qu’il s’agisse d’une technique informatique très répandue et très populaire, ne soit pas vraiment traité en détail dans les livres.
      Avec la croissance de l’IoT et des communications entre machines, et compte tenu des implémentations de CORDIC et de leur efficacité de calcul, son utilisation va probablement fortement augmenter ; il faut donc de bonnes références pour des implémentations correctes et optimisées.
      Les livres du Pr Omondi et du Pr Deschamps font exception.
      https://www.worldscientific.com/worldscibooks/10.1142/p1054
      http://www.arithmetic-circuits.org/guide2fpga/vhdl_codes.htm
  • sin et cos sont souvent utilisés pour les rotations de vecteurs.
    Dans ce cas, l’astuce avec CORDIC consiste à éviter le calcul traditionnel sin/cos/multiplication, et à fournir comme entrée à CORDIC le vecteur à faire tourner lui-même.
    CORDIC produit alors directement le vecteur tourné, sans calculer sin/cos ni effectuer de multiplication de nombres complexes.
    CORDIC brille particulièrement lorsque la latence n’est pas cruciale.
    En pipelineant chaque étape du calcul, on peut obtenir un débit élevé, ce qui convient bien au mixage numérique dans les systèmes radio.

  • En 2023, certains MCU modernes sont peu coûteux tout en disposant d’une FPU.
    Le STM32G4 en est un bon exemple et, contrairement à des MCU comme les M0, si l’on ne veut pas utiliser de virgule fixe, on peut employer librement f32.
    On peut se procurer ce type de puce pour environ 1 à 2 dollars par MCU.
    Cela dit, le G4 possède aussi un périphérique matériel CORDIC qui implémente cet algorithme pour les usages en virgule fixe.
    Je me demande si c’est principalement destiné à éviter les pertes de précision en virgule flottante.
    On le programme via des registres, mais ce n’est pas le CPU qui implémente directement CORDIC : c’est un matériel dédié à l’intérieur de l’IC qui s’en charge.

    • D’après les stocks Digi-Key, en excluant les doublons, les Cortex-M4F les moins chers sont à peu près le Nuvoton M481LE8AE à 3 dollars https://www.digikey.com/en/products/detail/nuvoton-technolog..., le Maxim MAX32660 à 3 dollars https://www.digikey.com/en/products/detail/analog-devices-in... et l’Atmel ATSAMD51 à 5 dollars https://www.digikey.com/en/products/detail/microchip-technol....
      Le STM32G4 le moins cher est le STM32G441KBT6, qui s’arrondit à 4 dollars https://www.digikey.com/en/products/detail/microchip-technol...
      Je me demande où l’on peut en trouver à moins de 2 dollars.
      Chez Digi-Key, la puce Nuvoton passe tout juste sous les 2 dollars pour une quantité de 500 pièces.
    • La deuxième puce Parallax Propeller possède un moteur CORDIC implémenté en silicium.
      Il est rapide et gère des produits intermédiaires sur 64 bits, ce qui donne une précision suffisante pour la division et les fonctions trigonométriques dans la plupart des usages.
      Si nécessaire, on peut encore augmenter la précision en logiciel.
      J’ai découvert CORDIC tardivement ; avant cela, j’utilisais beaucoup la virgule fixe dans le monde de l’assembleur 8 et 16 bits, pour les performances et le déterminisme.
      Quand je l’ai découvert, j’ai été surpris.
      C’était rapide, et les notions mathématiques nécessaires pour s’en servir utilement étaient seulement basiques.
  • Cela me rappelle un petit morceau de code assez mignon auquel j’avais participé il y a longtemps.
    Il fallait trouver les coordonnées de la bissectrice de l’angle formé par un arc du cercle unité, et on avait déjà les coordonnées (x,y) des deux côtés.
    L’implémentation existante était un gros bloc de trigonométrie qui convertissait les coordonnées (x,y) en coordonnées polaires (r,θ), vérifiait que le θ calculé était dans le bon quadrant, divisait θ par deux, puis reconvertissait en (x,y).
    Résultat : beaucoup d’appels à des fonctions trigonométriques et inverses.
    Comme c’était en Python et que les nombres complexes y sont des objets de première classe, il suffisait de définir deux nombres complexes z1 à partir de (x1,y1) et z2 à partir de (x2,y2), puis de prendre la moyenne géométrique du produit, √(z1*z2).
    Le nouveau code ne contenait ni fonction trigonométrique explicite, ni conversion et conversion inverse explicites.

  • Il est écrit : « il est assez évident que tourner de 22,75˚ revient à tourner de 45˚ puis de -22,5˚ ». Mais dans ce cas, ne s’agit-il pas d’une rotation de 22,5° ?
    Je me demande si c’est une erreur dans l’article, ou si c’est moi qui ai mal compris.

    • C’est une erreur dans l’article.
  • Le système d’octree de Meagher est connu pour n’utiliser que de l’arithmétique entière, sans multiplication ni division entières.
    « Des algorithmes efficaces en temps linéaire ont été développés pour les opérations booléennes (union, intersection, différence), les opérations géométriques (translation, redimensionnement, rotation), la détection d’interférences en N dimensions et l’affichage incluant l’élimination des surfaces cachées depuis n’importe quel point de l’espace. Ces algorithmes ne nécessitent ni opérations en virgule flottante, ni multiplication entière, ni division entière. »
    https://doi.org/10.1016/0146-664X(82)90104-6
    Grâce à cela, il était plus facile de créer du matériel d’accélération graphique VLSI rapide et spécialisé pour la représentation par octree.

  • Je me demande quelles performances offre CORDIC par rapport à une interpolation cubique utilisant une petite table, ou à d’autres interpolations polynomiales.
    J’ai appris que les synthétiseurs aux ressources limitées utilisaient parfois l’interpolation cubique, probablement à une époque où CORDIC était relativement récent.
    À vue de nez, CORDIC gagne 1 bit de précision à chaque itération, donc le calcul semble plus coûteux, mais il devrait utiliser moins d’espace que les polynômes.
    Cela dit, côté espace, il faut souligner qu’on peut faire encore moins cher que la table de correspondance de 4096 entrées pour sin(x) présentée dans l’article.
    Grâce aux symétries, seul 1/4 du cercle complet est nécessaire.

    • Les anciens développeurs de jeux et de la demoscene utilisaient à peine une table de correspondance de 256 entrées pour sin et cos.
      Avec des angles sur un octet, le bouclage se faisait automatiquement, ce qui était pratique, et 2^8 suffisait largement pour les rotations dans les jeux 2D.
      En revanche, si l’on voulait des mouvements fluides, cela n’allait pas très loin en 3D.