web-dev-qa-db-fra.com

Implémentation la plus rapide de sinus, cosinus et racine carrée en C ++ (n'a pas besoin d'être très précis)

Je recherche la question depuis une heure sur Google, mais il n'y a que des points sur Taylor Series ou un exemple de code qui est trop lent ou qui ne se compile pas du tout. Eh bien, la plupart des réponses que j'ai trouvées sur Google sont "Google, c'est déjà demandé", mais malheureusement ce n'est pas ...

Je suis en train de profiler mon jeu sur Pentium 4 bas de gamme et j'ai découvert que ~ 85% du temps d'exécution est gaspillé en calculant sinus, cosinus et racine carrée (à partir de la bibliothèque C++ standard dans Visual Studio), et cela semble dépendre fortement du processeur ( sur mon I7, les mêmes fonctions n'ont obtenu que 5% du temps d'exécution, et le jeu est waaaaaaaaaay plus rapide). Je ne peux pas optimiser ces trois fonctions, ni calculer le sinus et le cosinus en un seul passage (là interdépendant), mais je n'ai pas besoin de résultats trop précis pour ma simulation, donc je peux vivre avec une approximation plus rapide.

Donc, la question: Quel est le moyen le plus rapide pour calculer le sinus, le cosinus et la racine carrée de float en C++?

[~ # ~] modifier [~ # ~] La table de recherche est plus douloureuse car le cache Miss résultant est beaucoup plus coûteux sur le CPU moderne que sur la série Taylor. Les processeurs sont tellement rapides ces jours-ci, et le cache ne l'est pas.

J'ai fait une erreur, je pensais que je devais calculer plusieurs factorielles pour la série Taylor, et je vois maintenant qu'elles peuvent être implémentées sous forme de constantes.

Donc, la question mise à jour: existe-t-il également une optimisation rapide pour la racine carrée?

EDIT2

J'utilise la racine carrée pour calculer la distance, pas la normalisation - ne peut pas utiliser l'algorithme de racine carrée inverse rapide (comme indiqué dans le commentaire: http://en.wikipedia.org/wiki/Fast_inverse_square_root

EDIT3

Je ne peux pas non plus opérer sur des distances carrées, j'ai besoin d'une distance exacte pour les calculs

33
PiotrK

Le moyen le plus rapide consiste à précalculer les valeurs et à utiliser une table comme dans cet exemple:

Créer une table de recherche sinus en C++

MAIS si vous insistez sur le calcul à l'exécution, vous pouvez utiliser l'expansion de la série Taylor de sinus ou cosinus ...

Taylor Series of sine

Pour en savoir plus sur la série Taylor ... http://en.wikipedia.org/wiki/Taylor_series

L'une des clés pour que cela fonctionne bien est de pré-calculer les factorielles et de les tronquer à un nombre raisonnable de termes. Les factorielles augmentent très rapidement dans le dénominateur, vous n'avez donc pas besoin de porter plus de quelques termes.

Aussi ... Ne multipliez pas votre x ^ n depuis le début à chaque fois ... par exemple. multipliez x ^ 3 par x encore deux fois, puis par deux autres pour calculer les exposants.

17
KeatsKelleher

Premièrement, la série Taylor n'est PAS le moyen le meilleur/le plus rapide pour implémenter sinus/cos. Ce n'est pas non plus la façon dont les bibliothèques professionnelles implémentent ces fonctions trigonométriques, et connaître la meilleure implémentation numérique vous permet de régler la précision pour obtenir une vitesse plus efficacement. De plus, ce problème a déjà été largement discuté dans StackOverflow. Voici juste un exemple .

Deuxièmement, la grande différence que vous voyez entre l'ancien et le nouveau PCS est due au fait que l'architecture Intel moderne a un code d'assemblage explicite pour calculer les fonctions trigonométriques élémentaires. Il est assez difficile de les battre à la vitesse d'exécution.

Enfin, parlons du code sur votre ancien PC. Vérifiez bibliothèque scientifique gsl gn (ou recettes numériques), et vous verrez qu'ils utilisent essentiellement la formule d'approximation de Chebyshev.

L'approximation de Chebyshev converge plus rapidement, vous devez donc évaluer moins de termes. Je n'écrirai pas les détails d'implémentation ici car il y a déjà de très belles réponses publiées sur StackOverflow. Cochez celui-ci par exemple . Ajustez simplement le nombre de termes de cette série pour modifier l'équilibre entre la précision et la vitesse.

Pour ce type de problème, si vous voulez des détails d'implémentation d'une fonction spéciale ou d'une méthode numérique, vous devriez jeter un œil au code GSL avant toute autre action - GSL est LA bibliothèque numérique STANDARD.

EDIT: vous pouvez améliorer le temps d'exécution en incluant des indicateurs agressifs d'optimisation à virgule flottante dans gcc/icc. Cela diminuera la précision, mais il semble que c'est exactement ce que vous voulez.

EDIT2: Vous pouvez essayer de créer une grille de péché grossier et utiliser la routine gsl (gsl_interp_cspline_periodic pour la spline avec des conditions périodiques) pour spliner cette table (la spline réduira les erreurs par rapport à une interpolation linéaire => vous avez besoin de moins de points sur votre table = > moins de cache raté)!

40
Vivian Miranda

Voici la fonction sinusoïdale la plus rapide garantie en C++:

double FastSin(double x)
{
    return 0;
}

Oh, vous vouliez une meilleure précision que | 1.0 |? Bien lu.

Les ingénieurs des années 1970 ont fait des découvertes fantastiques dans ce domaine, mais les nouveaux programmeurs ignorent tout simplement que ces méthodes existent, car elles ne sont pas enseignées dans le cadre des programmes informatiques standard.

Vous devez commencer par comprendre que il n'y a pas d'implémentation "parfaite" de ces fonctions pour toutes les applications. Par conséquent, les réponses superficielles à des questions comme "laquelle est la plus rapide" sont assurément fausses.

La plupart des personnes qui posent cette question ne comprennent pas l'importance des compromis entre performances et précision . En particulier, vous devrez faire quelques choix concernant l'exactitude des calculs avant de faire quoi que ce soit d'autre. Quelle erreur pouvez-vous tolérer dans le résultat? 10 ^ -4? 10 ^ -16?

À moins que vous ne puissiez quantifier l'erreur dans n'importe quelle méthode, ne l'utilisez pas. Voir toutes ces réponses aléatoires ci-dessous, qui affichent un tas de sources aléatoires non commentées code, sans documenter clairement l'algorithme utilisé et son erreur maximale sur la plage d'entrée? C'est strictement une ligue de brousse. Si vous ne pouvez pas calculer l'erreur maximale exacte dans votre fonction sinus, vous ne pouvez pas écrire une fonction sinus.

Personne n'utilise la série Taylor seule pour approximer les transcendantaux dans les logiciels. À l'exception de certains cas très spécifiques, les séries de Taylor approchent généralement la cible lentement dans les plages d'entrée courantes.

Les algorithmes que vos grands-parents utilisaient pour calculer efficacement les transcendantaux sont collectivement appelés CORDIQUES et étaient suffisamment simples pour être implémentés dans le matériel. Voici une implémentation CORDIC bien documentée en C . Les implémentations CORDIC nécessitent généralement une très petite table de recherche, mais la plupart des implémentations n'exigent même pas qu'un multiplicateur matériel soit disponible. La plupart des implémentations CORDIC vous permettent de troquer les performances contre la précision, y compris celle que j'ai liée.

Il y a eu beaucoup d'améliorations incrémentielles des algorithmes CORDIC originaux au fil des ans. Par exemple, l'année dernière, certains chercheurs au Japon ont publié un article sur un CORDIC amélioré avec de meilleurs angles de rotation, ce qui réduit les opérations requises.

Si vous avez des multiplicateurs matériels (et vous en avez presque certainement), ou si vous ne pouvez pas vous permettre une table de recherche comme CORDIC l'exige, vous pouvez toujours utiliser un polynôme de Chebyshev pour faire la même chose. Les polynômes de Chebyshev nécessitent des multiplications, mais c'est rarement un problème sur le matériel moderne. Nous aimons les polynômes de Chebyshev parce que ils ont des erreurs maximales hautement prévisibles pour une approximation donnée . Le maximum du dernier terme d'un polynôme de Chebyshev, dans votre plage d'entrée, limite l'erreur dans le résultat. Et cette erreur diminue à mesure que le nombre de termes augmente. Voici un exemple d'un polynôme de Chebyshev donnant une approximation sinusoïdale sur une vaste plage, ignorant la symétrie naturelle de la fonction sinusoïdale et résolvant simplement le problème d'approximation en y jetant plus de coefficients.

Nous aimons également les polynômes de Chebyshev car l'erreur dans l'approximation est également répartie sur la plage de sorties. Si vous écrivez des plugins audio ou effectuez un traitement numérique du signal, les polynômes de Chebyshev vous offrent un effet de tramage bon marché et prévisible "gratuitement".

Si vous souhaitez trouver vos propres coefficients polynomiaux de Chebyshev dans une plage spécifique, de nombreuses bibliothèques mathématiques appellent le processus de recherche de ces coefficients " ajustement Chebyshev " ou quelque chose comme ça.

Les racines carrées, alors comme maintenant, sont généralement calculées avec une variante de algorithme de Newton-Raphson , généralement avec un nombre fixe d'itérations. Habituellement, quand quelqu'un lance un "incroyable nouveau" algorithme pour faire des racines carrées, c'est simplement Newton-Raphson déguisé.

Les polynômes de Newton-Raphson, CORDIC et Chebyshev vous permettent de comparer la vitesse pour la précision, de sorte que la réponse peut être aussi imprécise que vous le souhaitez.

Enfin, lorsque vous avez terminé toutes vos analyses comparatives et micro-optimisation, assurez-vous que votre version "rapide" est réellement plus rapide que la version bibliothèque. Voici une implémentation de bibliothèque typique de fsin () borné sur le domaine de -pi/4 à pi/4. Et ce n'est pas si lent que ça.

Il y a des gens qui ont consacré leur vie à résoudre ces problèmes efficacement, et ils ont produit des résultats fascinants. Lorsque vous êtes prêt à rejoindre l'ancienne école, prenez une copie de Recettes numériques .

tl: dr; go google "approximation sinus" ou "approximation cosinus" ou "approximation racine carrée" ou " théorie de l'approximation ."

27
johnwbyrd

Pour la racine carrée, il existe une approche appelée décalage de bits.

Un nombre flottant défini par IEEE-754 utilise un certain nombre de bits pour décrire des temps de multiples basés sur 2. Certains bits sont pour représenter la valeur de base.

float squareRoot(float x)
{
  unsigned int i = *(unsigned int*) &x;

  // adjust bias
  i  += 127 << 23;
  // approximation of square root
  i >>= 1;

  return *(float*) &i;
}

C'est un temps constant pour calculer la racine carrée

19
BigTailWolf

Basé sur l'idée de http://forum.devmaster.net/t/fast-and-accurate-sine-cosine/9648 et une réécriture manuelle pour améliorer les performances dans un micro benchmark, j'ai fini par avec l'implémentation cosinus suivante qui est utilisée dans une simulation physique HPC qui est goulot d'étranglement par des appels cos répétés sur un grand espace numérique. Il est suffisamment précis et beaucoup plus rapide qu'une table de recherche, notamment aucune division n'est requise.

template<typename T>
inline T cos(T x) noexcept
{
    constexpr T tp = 1./(2.*M_PI);
    x *= tp;
    x -= T(.25) + std::floor(x + T(.25));
    x *= T(16.) * (std::abs(x) - T(.5));
    #if EXTRA_PRECISION
    x += T(.225) * x * (std::abs(x) - T(1.));
    #endif
    return x;
}

Le compilateur Intel au moins est également assez intelligent pour vectoriser cette fonction lorsqu'il est utilisé dans une boucle.

Si EXTRA_PRECISION est défini, l'erreur maximale est d'environ 0,00109 pour la plage -π à π, en supposant que T est double comme il est généralement défini dans la plupart des implémentations C++. Sinon, l'erreur maximale est d'environ 0,056 pour la même plage.

10
milianw

Pour x86, le matériel FP les instructions de racine carrée sont rapides (sqrtss est sqrt Scalar Single-precision). La précision simple est plus rapide que la double précision, donc utilisez certainement float au lieu de double pour le code où vous pouvez vous permettre d'utiliser moins de précision.

Pour le code 32 bits, vous avez généralement besoin des options du compilateur pour le faire FP math avec SSE, plutôt que x87. (Par exemple -mfpmath=sse )

Pour que les fonctions sqrt() ou sqrtf() de C s'alignent en tant que sqrtsd ou sqrtss, , vous devez compiler avec -fno-math-errno. La définition de fonctions mathématiques errno sur NaN est généralement considérée comme une erreur de conception, mais la norme l'exige. Sans cette option, gcc l'inline mais fait ensuite une branche compare + pour voir si le résultat était NaN, et si c'est le cas, appelle la fonction de bibliothèque afin qu'elle puisse définir errno. Si votre programme ne vérifie pas errno après les fonctions mathématiques, il n'y a aucun danger à utiliser -fno-math-errno.

Vous n'avez besoin d'aucune des parties "non sûres" de -ffast-math Pour obtenir sqrt et d'autres fonctions pour mieux les aligner ou pas du tout, mais -ffast-math Peut faire une grande différence (par exemple, permettre au compilateur de vectoriser automatiquement dans les cas où cela change le résultat, car les mathématiques FP ne sont pas associatives .

par exemple. avec gcc6.3 compilant float foo(float a){ return sqrtf(a); }

foo:    # with -O3 -fno-math-errno.
    sqrtss  xmm0, xmm0
    ret
foo:   # with just -O3
    pxor    xmm2, xmm2   # clang just checks for NaN, instead of comparing against zero.
    sqrtss  xmm1, xmm0
    ucomiss xmm2, xmm0
    ja      .L8          # take the slow path if 0.0 > a
    movaps  xmm0, xmm1
    ret

.L8:                     # errno-setting path
    sub     rsp, 24
    movss   DWORD PTR [rsp+12], xmm1   # store the sqrtss result because the x86-64 SysV ABI has no call-preserved xmm regs.
    call    sqrtf                      # call sqrtf just to set errno
    movss   xmm1, DWORD PTR [rsp+12]
    add     rsp, 24
    movaps  xmm0, xmm1    # extra mov because gcc reloaded into the wrong register.
    ret

le code de gcc pour le cas NaN semble bien trop compliqué; il n'utilise même pas la valeur de retour sqrtf! Quoi qu'il en soit, c'est le genre de gâchis que vous obtenez réellement sans -fno-math-errno, Pour chaque site d'appel sqrtf() dans votre programme. La plupart du temps, c'est juste du code, et aucun bloc .L8 Ne s'exécutera jamais lorsque vous prenez le sqrt d'un nombre> = 0,0, mais il y a encore plusieurs instructions supplémentaires dans le chemin rapide.


Si vous savez que votre entrée dans sqrt est non nulle, vous pouvez utiliser l'instruction sqrt réciproque rapide mais très approximative, rsqrtps (ou rsqrtss pour la version scalaire). Un itération de Newton-Raphson l'amène à presque la même précision que l'instruction matérielle simple précision sqrt, mais pas tout à fait.

sqrt(x) = x * 1/sqrt(x), pour x!=0, vous pouvez donc calculer un sqrt avec rsqrt et une multiplication. Ce sont tous les deux rapides, même sur P4 (était-ce toujours d'actualité en 2013)?

Sur P4, il peut être utile d'utiliser rsqrt + itération de Newton pour remplacer un seul sqrt, même si vous n'avez pas besoin de diviser quoi que ce soit par celui-ci.

Voir aussi ne réponse que j'ai écrite récemment à propos de la gestion des zéros lors du calcul de sqrt(x) comme x*rsqrt(x) , avec une itération de Newton. J'ai inclus une discussion sur l'erreur d'arrondi si vous souhaitez convertir la valeur FP en entier) et des liens vers d'autres questions pertinentes.


P4:

  • sqrtss: latence 23c, non canalisé
  • sqrtsd: latence 38c, non canalisé
  • fsqrt (x87): latence 43c, non canalisé
  • rsqrtss/mulss: latence 4c + 6c. Peut-être un par débit 3c, car ils n'ont apparemment pas besoin de la même unité d'exécution (mmx vs fp).

  • Les versions SIMD sont un peu plus lentes


Skylake:

  • sqrtss/sqrtps: latence 12c, une par débit 3c
  • sqrtsd/sqrtpd: latence 15-16c, une par débit 4-6c
  • fsqrt (x87): latence 14-21cc, une par débit 4-7c
  • rsqrtss/mulss: latence 4c + 4c. Un par débit 1c.
  • Les versions vectorielles SIMD 128b ont la même vitesse. Les versions vectorielles 256b ont une latence un peu plus élevée, près de la moitié du débit. La version rsqrtss a des performances complètes pour les vecteurs 256b.

Avec une itération de Newton, la version rsqrt n'est pas beaucoup plus rapide.


Chiffres de tests expérimentaux d'Agner Fog . Consultez ses guides microarch pour comprendre ce qui fait que le code s'exécute rapidement ou lentement. Voir également les liens sur le wiki de la balise x86 .

IDK comment calculer au mieux sin/cos. J'ai lu que le matériel fsin/fcos (et le seul fsincos légèrement plus lent qui fait les deux à la fois) n'est pas le moyen le plus rapide, mais IDK ce qui est.

5
Peter Cordes

QT a des implémentations rapides de sinus (qFastSin) et de cosinus (qFastCos) qui utilisent la table de recherche avec interpolation. Je l'utilise dans mon code et ils sont plus rapides que std: sin/cos et assez précis pour ce dont j'ai besoin (erreur ~ = 0,01% je suppose):

https://code.woboq.org/qt5/qtbase/src/corelib/kernel/qmath.h.html#_Z8qFastSind

#define QT_SINE_TABLE_SIZE 256


inline qreal qFastSin(qreal x)
{
   int si = int(x * (0.5 * QT_SINE_TABLE_SIZE / M_PI)); // Would be more accurate with qRound, but slower.
   qreal d = x - si * (2.0 * M_PI / QT_SINE_TABLE_SIZE);
   int ci = si + QT_SINE_TABLE_SIZE / 4;
   si &= QT_SINE_TABLE_SIZE - 1;
   ci &= QT_SINE_TABLE_SIZE - 1;
   return qt_sine_table[si] + (qt_sine_table[ci] - 0.5 * qt_sine_table[si] * d) * d;
}

inline qreal qFastCos(qreal x)
{
   int ci = int(x * (0.5 * QT_SINE_TABLE_SIZE / M_PI)); // Would be more accurate with qRound, but slower.
   qreal d = x - ci * (2.0 * M_PI / QT_SINE_TABLE_SIZE);
   int si = ci + QT_SINE_TABLE_SIZE / 4;
   si &= QT_SINE_TABLE_SIZE - 1;
   ci &= QT_SINE_TABLE_SIZE - 1;
   return qt_sine_table[si] - (qt_sine_table[ci] + 0.5 * qt_sine_table[si] * d) * d;
}

La LUT et la licence peuvent être trouvées ici: https://code.woboq.org/qt5/qtbase/src/corelib/kernel/qmath.cpp.html#qt_sine_table

3
Adriel Jr

Ceci est une implémentation de la méthode Taylor Series précédemment donnée dans réponse d'Akellehe .

unsigned int Math::SIN_LOOP = 15;
unsigned int Math::COS_LOOP = 15;

// sin(x) = x - x^3/3! + x^5/5! - x^7/7! + ...
template <class T>
T Math::sin(T x)
{
    T Sum       = 0;
    T Power     = x;
    T Sign      = 1;
    const T x2  = x * x;
    T Fact      = 1.0;
    for (unsigned int i=1; i<SIN_LOOP; i+=2)
    {
        Sum     += Sign * Power / Fact;
        Power   *= x2;
        Fact    *= (i + 1) * (i + 2);
        Sign    *= -1.0;
    }
    return Sum;
}

// cos(x) = 1 - x^2/2! + x^4/4! - x^6/6! + ...
template <class T>
T Math::cos(T x)
{
    T Sum       = x;
    T Power     = x;
    T Sign      = 1.0;
    const T x2  = x * x;
    T Fact      = 1.0;
    for (unsigned int i=3; i<COS_LOOP; i+=2)
    {
        Power   *= x2;
        Fact    *= i * (i - 1);
        Sign    *= -1.0;
        Sum     += Sign * Power / Fact;
    }
    return Sum;
}
1
hkBattousai

Partageant mon code, c'est un polynôme du 6ème degré, rien de spécial mais réarrangé pour éviter les pows. Sur Core i7, cela est 2,3 fois plus lent que l'implémentation standard, bien qu'un peu plus rapide pour la plage [0..2 * PI]. Pour un ancien processeur, cela pourrait être une alternative au sin/cos standard.

/*
    On [-1000..+1000] range with 0.001 step average error is: +/- 0.000011, max error: +/- 0.000060
    On [-100..+100] range with 0.001 step average error is:   +/- 0.000009, max error: +/- 0.000034
    On [-10..+10] range with 0.001 step average error is:     +/- 0.000009, max error: +/- 0.000030
    Error distribution ensures there's no discontinuity.
*/

const double PI          = 3.141592653589793;
const double HALF_PI     = 1.570796326794897;
const double DOUBLE_PI   = 6.283185307179586;
const double SIN_CURVE_A = 0.0415896;
const double SIN_CURVE_B = 0.00129810625032;

double cos1(double x) {
    if (x < 0) {
        int q = -x / DOUBLE_PI;
        q += 1;
        double y = q * DOUBLE_PI;
        x = -(x - y);
    }
    if (x >= DOUBLE_PI) {
        int q = x / DOUBLE_PI;
        double y = q * DOUBLE_PI;
        x = x - y;
    }
    int s = 1;
    if (x >= PI) {
        s = -1;
        x -= PI;
    }
    if (x > HALF_PI) {
        x = PI - x;
        s = -s;
    }
    double z = x * x;
    double r = z * (z * (SIN_CURVE_A - SIN_CURVE_B * z) - 0.5) + 1.0;
    if (r > 1.0) r = r - 2.0;
    if (s > 0) return r;
    else return -r;
}

double sin1(double x) {
    return cos1(x - HALF_PI);
}
1
Josh

Utilisez simplement le FPU avec x86 en ligne pour les applications Wintel. Il semblerait que la fonction sqrt du CPU direct bat toujours la vitesse de tout autre algorithme. Mon code de bibliothèque mathématique x86 personnalisé est pour MSVC++ 2005 et versions ultérieures. Vous avez besoin de versions flottantes/doubles séparées si vous voulez plus de précision que j'ai couverte. Parfois, la stratégie "__inline" du compilateur tourne mal, donc pour être sûr, vous pouvez la supprimer. Avec l'expérience, vous pouvez passer aux macros pour éviter totalement un appel de fonction à chaque fois.

extern __inline float  __fastcall fs_sin(float x);
extern __inline double __fastcall fs_Sin(double x);
extern __inline float  __fastcall fs_cos(float x);
extern __inline double __fastcall fs_Cos(double x);
extern __inline float  __fastcall fs_atan(float x);
extern __inline double __fastcall fs_Atan(double x);
extern __inline float  __fastcall fs_sqrt(float x);
extern __inline double __fastcall fs_Sqrt(double x);
extern __inline float  __fastcall fs_log(float x);
extern __inline double __fastcall fs_Log(double x);

extern __inline float __fastcall fs_sqrt(float x) { __asm {
FLD x  ;// Load/Push input value
FSQRT
}}

extern __inline double __fastcall fs_Sqrt(double x) { __asm {
FLD x  ;// Load/Push input value
FSQRT
}}

extern __inline float __fastcall fs_sin(float x) { __asm {
FLD x  ;// Load/Push input value
FSIN
}}

extern __inline double __fastcall fs_Sin(double x) { __asm {
FLD x  ;// Load/Push input value
FSIN
}}    

extern __inline float __fastcall fs_cos(float x) { __asm {
FLD x  ;// Load/Push input value
FCOS
}}

extern __inline double __fastcall fs_Cos(double x) { __asm {
FLD x  ;// Load/Push input value
FCOS
}}

extern __inline float __fastcall fs_tan(float x) { __asm {
FLD x  ;// Load/Push input value
FPTAN
}}

extern __inline double __fastcall fs_Tan(double x) { __asm {
FLD x  ;// Load/Push input value
FPTAN
}}

extern __inline float __fastcall fs_log(float x) { __asm {
FLDLN2
FLD x
FYL2X
FSTP ST(1) ;// Pop1, Pop2 occurs on return
}}

extern __inline double __fastcall fs_Log(double x) { __asm {
FLDLN2
FLD x
FYL2X
FSTP ST(1) ;// Pop1, Pop2 occurs on return
}}
1
John Doe

J'utilise le code suivant pour calculer des fonctions trigonométriques en quadruple précision. La constante N détermine le nombre de bits de précision requis (par exemple N = 26 donnera une précision de précision simple). Selon la précision souhaitée, le stockage précalculé peut être petit et peut tenir dans le cache. Il ne nécessite que des opérations d'addition et de multiplication et est également très facile à vectoriser.

L'algorithme pré-calcule les valeurs sin et cos pour 0,5 ^ i, i = 1, ..., N. Ensuite, nous pouvons combiner ces valeurs précalculées, pour calculer sin et cos pour n'importe quel angle jusqu'à une résolution de 0,5 ^ N

template <class QuadReal_t>
QuadReal_t sin(const QuadReal_t a){
  const int N=128;
  static std::vector<QuadReal_t> theta;
  static std::vector<QuadReal_t> sinval;
  static std::vector<QuadReal_t> cosval;
  if(theta.size()==0){
    #pragma omp critical (QUAD_SIN)
    if(theta.size()==0){
      theta.resize(N);
      sinval.resize(N);
      cosval.resize(N);

      QuadReal_t t=1.0;
      for(int i=0;i<N;i++){
        theta[i]=t;
        t=t*0.5;
      }

      sinval[N-1]=theta[N-1];
      cosval[N-1]=1.0-sinval[N-1]*sinval[N-1]/2;
      for(int i=N-2;i>=0;i--){
        sinval[i]=2.0*sinval[i+1]*cosval[i+1];
        cosval[i]=sqrt(1.0-sinval[i]*sinval[i]);
      }
    }
  }

  QuadReal_t t=(a<0.0?-a:a);
  QuadReal_t sval=0.0;
  QuadReal_t cval=1.0;
  for(int i=0;i<N;i++){
    while(theta[i]<=t){
      QuadReal_t sval_=sval*cosval[i]+cval*sinval[i];
      QuadReal_t cval_=cval*cosval[i]-sval*sinval[i];
      sval=sval_;
      cval=cval_;
      t=t-theta[i];
    }
  }
  return (a<0.0?-sval:sval);
}
1
Dhairya

Sur 100000000 test, la réponse milianw est 2 fois plus lente que l'implémentation std :: cos. Cependant, vous pouvez réussir à l'exécuter plus rapidement en procédant comme suit:

-> utiliser un flotteur

-> n'utilisez pas le plancher mais static_cast

-> ne pas utiliser abs mais conditionnel ternaire

-> utiliser la constante #define pour la division

-> utiliser une macro pour éviter l'appel de fonction

// 1 / (2 * PI)
#define FPII 0.159154943091895
//PI / 2
#define PI2 1.570796326794896619

#define _cos(x)         x *= FPII;\
                        x -= .25f + static_cast<int>(x + .25f) - 1;\
                        x *= 16.f * ((x >= 0 ? x : -x) - .5f);
#define _sin(x)         x -= PI2; _cos(x);

Plus de 100000000 appels à std :: cos et _cos (x), std :: cos s'exécutent sur ~ 14s vs ~ 3s pour _cos (x) (un peu plus pour _sin (x))

1
Hugo Zevetel

Permettez-moi donc de reformuler cela, cette idée vient d'approximer les fonctions cosinus et sinus sur un intervalle [-pi/4, + pi/4] avec une erreur bornée en utilisant l'algorithme Remez. Ensuite, en utilisant le reste flottant à plage réduite et une LUT pour les sorties cos & sinus du quotient entier, l'approximation peut être déplacée vers n'importe quel argument angular angulaire).

C'est tout simplement unique et j'ai pensé qu'il pourrait être développé pour créer un algorithme plus efficace en termes d'erreur bornée.

void sincos_fast(float x, float *pS, float *pC){
     float cosOff4LUT[] = { 0x1.000000p+00,  0x1.6A09E6p-01,  0x0.000000p+00, -0x1.6A09E6p-01, -0x1.000000p+00, -0x1.6A09E6p-01,  0x0.000000p+00,  0x1.6A09E6p-01 };

    int     m, ms, mc;
    float   xI, xR, xR2;
    float   c, s, cy, sy;

    // Cody & Waite's range reduction Algorithm, [-pi/4, pi/4]
    xI  = floorf(x * 0x1.45F306p+00 + 0.5);
    xR  = (x - xI * 0x1.920000p-01) - xI*0x1.FB5444p-13;
    m   = (int) xI;
    xR2 = xR*xR;

    // Find cosine & sine index for angle offsets indices
    mc = (  m  ) & 0x7;     // two's complement permits upper modulus for negative numbers =P
    ms = (m + 6) & 0x7;     // two's complement permits upper modulus for negative numbers =P, note phase correction for sine.

    // Find cosine & sine
    cy = cosOff4LUT[mc];     // Load angle offset neighborhood cosine value 
    sy = cosOff4LUT[ms];     // Load angle offset neighborhood sine value 

    c = 0xf.ff79fp-4 + xR2 * (-0x7.e58e9p-4);               // TOL = 1.2786e-4
    // c = 0xf.ffffdp-4 + xR2 * (-0x7.ffebep-4 + xR2 * 0xa.956a9p-8);  // TOL = 1.7882e-7

     s = xR * (0xf.ffbf7p-4 + x2 * (-0x2.a41d0cp-4));   // TOL = 4.835251e-6
    // s = xR * (0xf.fffffp-4 + xR2 * (-0x2.aaa65cp-4 + xR2 * 0x2.1ea25p-8));  // TOL = 1.1841e-8

     *pC = c*cy - s*sy;     
    *pS = c*sy + s*cy;
}

float sqrt_fast(float x){
    union {float f; int i; } X, Y;
    float ScOff;
    uint8_t e;

    X.f = x;
    e = (X.i >> 23);           // f.SFPbits.e;

    if(x <= 0) return(0.0f);

    ScOff = ((e & 1) != 0) ? 1.0f : 0x1.6a09e6p0;  // NOTE: If exp=EVEN, b/c (exp-127) a (EVEN - ODD) := ODD; but a (ODD - ODD) := EVEN!!

    e = ((e + 127) >> 1);                            // NOTE: If exp=ODD,  b/c (exp-127) then flr((exp-127)/2)
    X.i = (X.i & ((1uL << 23) - 1)) | (0x7F << 23);  // Mask mantissa, force exponent to zero.
    Y.i = (((uint32_t) e) << 23);

    // Error grows with square root of the exponent. Unfortunately no work around like inverse square root... :(
    // Y.f *= ScOff * (0x9.5f61ap-4 + X.f*(0x6.a09e68p-4));        // Error = +-1.78e-2 * 2^(flr(log2(x)/2))
    // Y.f *= ScOff * (0x7.2181d8p-4 + X.f*(0xa.05406p-4 + X.f*(-0x1.23a14cp-4)));      // Error = +-7.64e-5 * 2^(flr(log2(x)/2))
    // Y.f *= ScOff * (0x5.f10e7p-4 + X.f*(0xc.8f2p-4 +X.f*(-0x2.e41a4cp-4 + X.f*(0x6.441e6p-8))));     // Error =  8.21e-5 * 2^(flr(log2(x)/2))
    // Y.f *= ScOff * (0x5.32eb88p-4 + X.f*(0xe.abbf5p-4 + X.f*(-0x5.18ee2p-4 + X.f*(0x1.655efp-4 + X.f*(-0x2.b11518p-8)))));   // Error = +-9.92e-6 * 2^(flr(log2(x)/2))
    // Y.f *= ScOff * (0x4.adde5p-4 + X.f*(0x1.08448cp0 + X.f*(-0x7.ae1248p-4 + X.f*(0x3.2cf7a8p-4 + X.f*(-0xc.5c1e2p-8 + X.f*(0x1.4b6dp-8))))));   // Error = +-1.38e-6 * 2^(flr(log2(x)/2))
    // Y.f *= ScOff * (0x4.4a17fp-4 + X.f*(0x1.22d44p0 + X.f*(-0xa.972e8p-4 + X.f*(0x5.dd53fp-4 + X.f*(-0x2.273c08p-4 + X.f*(0x7.466cb8p-8 + X.f*(-0xa.ac00ep-12)))))));    // Error = +-2.9e-7 * 2^(flr(log2(x)/2))
    Y.f *= ScOff * (0x3.fbb3e8p-4 + X.f*(0x1.3b2a3cp0 + X.f*(-0xd.cbb39p-4 + X.f*(0x9.9444ep-4 + X.f*(-0x4.b5ea38p-4 + X.f*(0x1.802f9ep-4 + X.f*(-0x4.6f0adp-8 + X.f*(0x5.c24a28p-12 ))))))));   // Error = +-2.7e-6 * 2^(flr(log2(x)/2))

    return(Y.f);
}
0
nimig18