web-dev-qa-db-fra.com

Optimisations pour pow () avec const exposant non entier?

J'ai des points chauds dans mon code où je travaille pow(), ce qui représente environ 10 à 20% de mon temps d'exécution.

Mon entrée dans pow(x,y) étant très spécifique, je me demande s’il existe un moyen de lancer deux approximations pow() (une par exposant) plus performantes:

  • J'ai deux exposants constants: 2.4 et 1/2.4.
  • Lorsque l'exposant est égal à 2,4, x sera dans l'intervalle (0.090473935, 1.0].
  • Lorsque l'exposant est 1/2.4, x sera dans l'intervalle (0.0031308, 1.0].
  • J'utilise les vecteurs SSE/AVX float. Si les spécificités de la plate-forme peuvent être exploitées, c'est bien!

Un taux d'erreur maximal autour de 0,01% est idéal, même si les algorithmes de précision complète (pour float) m'intéressent également.

J'utilise déjà une rapide pow()approximation , mais elle ne prend pas ces contraintes en compte. Est-il possible de faire mieux?

57
Cory Nelson

Dans la veine de piratage IEEE 754, voici une autre solution plus rapide et moins "magique". Il atteint une marge d'erreur de 0,08% en une douzaine de cycles d'horloge (dans le cas de p = 2,4, sur un processeur Intel Merom).

Les nombres à virgule flottante ont été inventés à l'origine comme une approximation des logarithmes. Vous pouvez donc utiliser la valeur entière comme une approximation de log2. Cela est réalisable de manière quelque peu portable en appliquant l'instruction convert-from-integer à une valeur à virgule flottante pour obtenir une autre valeur à virgule flottante.

Pour compléter le calcul pow, vous pouvez multiplier par un facteur constant et reconvertir le logarithme avec l'instruction convert-to-integer. Sur SSE, les instructions appropriées sont cvtdq2ps et cvtps2dq.

Ce n'est pas si simple, cependant. Le champ exposant dans IEEE 754 est signé, avec une valeur de biais de 127 représentant un exposant de zéro. Ce biais doit être éliminé avant de multiplier le logarithme et ajouté de nouveau avant d'exposer. De plus, l'ajustement du biais par soustraction ne fonctionnera pas à zéro. Heureusement, les deux ajustements peuvent être obtenus en multipliant préalablement par un facteur constant.

x^p
= exp2( p * log2( x ) )
= exp2( p * ( log2( x ) + 127 - 127 ) - 127 + 127 )
= cvtps2dq( p * ( log2( x ) + 127 - 127 - 127 / p ) )
= cvtps2dq( p * ( log2( x ) + 127 - log2( exp2( 127 - 127 / p ) ) )
= cvtps2dq( p * ( log2( x * exp2( 127 / p - 127 ) ) + 127 ) )
= cvtps2dq( p * ( cvtdq2ps( x * exp2( 127 / p - 127 ) ) ) )

exp2( 127 / p - 127 ) est le facteur constant. Cette fonction est plutôt spécialisée: elle ne fonctionnera pas avec de petits exposants fractionnaires, car le facteur constant croît de manière exponentielle avec l'inverse de l'exposant et débordera. Cela ne fonctionnera pas avec les exposants négatifs. Les grands exposants entraînent une erreur élevée, car les bits de mantisse sont mélangés aux bits des exposants par la multiplication.

Mais, il ne reste que 4 instructions rapides. Pré-multiplier, convertir de "entier" (en logarithme), multiplier par puissance, convertir en "entier" (à partir de logarithme). Les conversions sont très rapides sur cette implémentation de SSE. Nous pouvons également insérer un coefficient constant supplémentaire dans la première multiplication.

template< unsigned expnum, unsigned expden, unsigned coeffnum, unsigned coeffden >
__m128 fastpow( __m128 arg ) {
        __m128 ret = arg;
//      std::printf( "arg = %,vg\n", ret );
        // Apply a constant pre-correction factor.
        ret = _mm_mul_ps( ret, _mm_set1_ps( exp2( 127. * expden / expnum - 127. )
                * pow( 1. * coeffnum / coeffden, 1. * expden / expnum ) ) );
//      std::printf( "scaled = %,vg\n", ret );
        // Reinterpret arg as integer to obtain logarithm.
        asm ( "cvtdq2ps %1, %0" : "=x" (ret) : "x" (ret) );
//      std::printf( "log = %,vg\n", ret );
        // Multiply logarithm by power.
        ret = _mm_mul_ps( ret, _mm_set1_ps( 1. * expnum / expden ) );
//      std::printf( "powered = %,vg\n", ret );
        // Convert back to "integer" to exponentiate.
        asm ( "cvtps2dq %1, %0" : "=x" (ret) : "x" (ret) );
//      std::printf( "result = %,vg\n", ret );
        return ret;
}

Quelques essais avec exposant = 2,4 montrent que cela surestime constamment d'environ 5%. (La routine est toujours garantie de surestimer.) Vous pouvez simplement multiplier par 0,95, mais quelques instructions supplémentaires nous permettront d'obtenir environ 4 chiffres décimaux de précision, ce qui devrait suffire pour les graphiques.

La solution consiste à faire correspondre la surestimation à une sous-estimation et à prendre la moyenne.

  • Calculez x ^ 0.8: quatre instructions, erreur ~ + 3%.
  • Calculez x ^ -0.4: une rsqrtps. (Ceci est assez précis, mais sacrifie la possibilité de travailler avec zéro.)
  • Calculez x ^ 0.4: une mulps.
  • Calculez x ^ -0.2: une rsqrtps.
  • Calculez x ^ 2: une mulps.
  • Calculez x ^ 3: une mulps.
  • x ^ 2.4 = x ^ 2 * x ^ 0.4: un mulps. C'est la surestimation.
  • x ^ 2.4 = x ^ 3 * x ^ -0,4 * x ^ -0,2: deux mulps. C'est la sous-estimation.
  • Moyenne de ce qui précède: une addps, une mulps.

Comptage des instructions: quatorze, y compris deux conversions avec une latence = 5 et deux estimations de racine carrée réciproques avec un débit = 4.

Pour bien prendre la moyenne, nous voulons pondérer les estimations par leurs erreurs attendues. La sous-estimation soulève l’erreur à une puissance de 0,6 vs 0,4, nous nous attendons donc à ce qu’elle soit 1,5 fois plus erronée. La pondération n'ajoute aucune instruction. cela peut être fait dans le pré-facteur. Appel du coefficient a: a ^ 0,5 = 1,5 a ^ -0,75 et a = 1,38316186.

L'erreur finale est d'environ 0,015%, soit deux ordres de grandeur supérieurs au résultat initial fastpow. Le temps d'exécution est d'environ une douzaine de cycles pour une boucle occupée avec des variables source et de destination volatile… même si elle chevauche les itérations, l'utilisation dans le monde réel verra également un parallélisme au niveau des instructions. En considérant SIMD, cela donne un résultat scalaire sur 3 cycles!

int main() {
        __m128 const x0 = _mm_set_ps( 0.01, 1, 5, 1234.567 );
        std::printf( "Input: %,vg\n", x0 );

        // Approx 5% accuracy from one call. Always an overestimate.
        __m128 x1 = fastpow< 24, 10, 1, 1 >( x0 );
        std::printf( "Direct x^2.4: %,vg\n", x1 );

        // Lower exponents provide lower initial error, but too low causes overflow.
        __m128 xf = fastpow< 8, 10, int( 1.38316186 * 1e9 ), int( 1e9 ) >( x0 );
        std::printf( "1.38 x^0.8: %,vg\n", xf );

        // Imprecise 4-cycle sqrt is still far better than fastpow, good enough.
        __m128 xfm4 = _mm_rsqrt_ps( xf );
        __m128 xf4 = _mm_mul_ps( xf, xfm4 );

        // Precisely calculate x^2 and x^3
        __m128 x2 = _mm_mul_ps( x0, x0 );
        __m128 x3 = _mm_mul_ps( x2, x0 );

        // Overestimate of x^2 * x^0.4
        x2 = _mm_mul_ps( x2, xf4 );

        // Get x^-0.2 from x^0.4. Combine with x^-0.4 into x^-0.6 and x^2.4.
        __m128 xfm2 = _mm_rsqrt_ps( xf4 );
        x3 = _mm_mul_ps( x3, xfm4 );
        x3 = _mm_mul_ps( x3, xfm2 );

        std::printf( "x^2 * x^0.4: %,vg\n", x2 );
        std::printf( "x^3 / x^0.6: %,vg\n", x3 );
        x2 = _mm_mul_ps( _mm_add_ps( x2, x3 ), _mm_set1_ps( 1/ 1.960131704207789 ) );
        // Final accuracy about 0.015%, 200x better than x^0.8 calculation.
        std::printf( "average = %,vg\n", x2 );
}

Eh bien… désolé, je n'ai pas pu poster ceci plus tôt. Et l’étendre à x ^ 1/2.4 est laissé comme exercice; v).


Mise à jour avec les statistiques

J'ai mis en place un petit harnais de test et deux x(5/12) cas correspondant à ce qui précède.

#include <cstdio>
#include <xmmintrin.h>
#include <cmath>
#include <cfloat>
#include <algorithm>
using namespace std;

template< unsigned expnum, unsigned expden, unsigned coeffnum, unsigned coeffden >
__m128 fastpow( __m128 arg ) {
    __m128 ret = arg;
//  std::printf( "arg = %,vg\n", ret );
    // Apply a constant pre-correction factor.
    ret = _mm_mul_ps( ret, _mm_set1_ps( exp2( 127. * expden / expnum - 127. )
        * pow( 1. * coeffnum / coeffden, 1. * expden / expnum ) ) );
//  std::printf( "scaled = %,vg\n", ret );
    // Reinterpret arg as integer to obtain logarithm.
    asm ( "cvtdq2ps %1, %0" : "=x" (ret) : "x" (ret) );
//  std::printf( "log = %,vg\n", ret );
    // Multiply logarithm by power.
    ret = _mm_mul_ps( ret, _mm_set1_ps( 1. * expnum / expden ) );
//  std::printf( "powered = %,vg\n", ret );
    // Convert back to "integer" to exponentiate.
    asm ( "cvtps2dq %1, %0" : "=x" (ret) : "x" (ret) );
//  std::printf( "result = %,vg\n", ret );
    return ret;
}

__m128 pow125_4( __m128 arg ) {
    // Lower exponents provide lower initial error, but too low causes overflow.
    __m128 xf = fastpow< 4, 5, int( 1.38316186 * 1e9 ), int( 1e9 ) >( arg );

    // Imprecise 4-cycle sqrt is still far better than fastpow, good enough.
    __m128 xfm4 = _mm_rsqrt_ps( xf );
    __m128 xf4 = _mm_mul_ps( xf, xfm4 );

    // Precisely calculate x^2 and x^3
    __m128 x2 = _mm_mul_ps( arg, arg );
    __m128 x3 = _mm_mul_ps( x2, arg );

    // Overestimate of x^2 * x^0.4
    x2 = _mm_mul_ps( x2, xf4 );

    // Get x^-0.2 from x^0.4, and square it for x^-0.4. Combine into x^-0.6.
    __m128 xfm2 = _mm_rsqrt_ps( xf4 );
    x3 = _mm_mul_ps( x3, xfm4 );
    x3 = _mm_mul_ps( x3, xfm2 );

    return _mm_mul_ps( _mm_add_ps( x2, x3 ), _mm_set1_ps( 1/ 1.960131704207789 * 0.9999 ) );
}

__m128 pow512_2( __m128 arg ) {
    // 5/12 is too small, so compute the sqrt of 10/12 instead.
    __m128 x = fastpow< 5, 6, int( 0.992245 * 1e9 ), int( 1e9 ) >( arg );
    return _mm_mul_ps( _mm_rsqrt_ps( x ), x );
}

__m128 pow512_4( __m128 arg ) {
    // 5/12 is too small, so compute the 4th root of 20/12 instead.
    // 20/12 = 5/3 = 1 + 2/3 = 2 - 1/3. 2/3 is a suitable argument for fastpow.
    // weighting coefficient: a^-1/2 = 2 a; a = 2^-2/3
    __m128 xf = fastpow< 2, 3, int( 0.629960524947437 * 1e9 ), int( 1e9 ) >( arg );
    __m128 xover = _mm_mul_ps( arg, xf );

    __m128 xfm1 = _mm_rsqrt_ps( xf );
    __m128 x2 = _mm_mul_ps( arg, arg );
    __m128 xunder = _mm_mul_ps( x2, xfm1 );

    // sqrt2 * over + 2 * sqrt2 * under
    __m128 xavg = _mm_mul_ps( _mm_set1_ps( 1/( 3 * 0.629960524947437 ) * 0.999852 ),
                                _mm_add_ps( xover, xunder ) );

    xavg = _mm_mul_ps( xavg, _mm_rsqrt_ps( xavg ) );
    xavg = _mm_mul_ps( xavg, _mm_rsqrt_ps( xavg ) );
    return xavg;
}

__m128 mm_succ_ps( __m128 arg ) {
    return (__m128) _mm_add_epi32( (__m128i) arg, _mm_set1_epi32( 4 ) );
}

void test_pow( double p, __m128 (*f)( __m128 ) ) {
    __m128 arg;

    for ( arg = _mm_set1_ps( FLT_MIN / FLT_EPSILON );
            ! isfinite( _mm_cvtss_f32( f( arg ) ) );
            arg = mm_succ_ps( arg ) ) ;

    for ( ; _mm_cvtss_f32( f( arg ) ) == 0;
            arg = mm_succ_ps( arg ) ) ;

    std::printf( "Domain from %g\n", _mm_cvtss_f32( arg ) );

    int n;
    int const bucket_size = 1 << 25;
    do {
        float max_error = 0;
        double total_error = 0, cum_error = 0;
        for ( n = 0; n != bucket_size; ++ n ) {
            float result = _mm_cvtss_f32( f( arg ) );

            if ( ! isfinite( result ) ) break;

            float actual = ::powf( _mm_cvtss_f32( arg ), p );

            float error = ( result - actual ) / actual;
            cum_error += error;
            error = std::abs( error );
            max_error = std::max( max_error, error );
            total_error += error;

            arg = mm_succ_ps( arg );
        }

        std::printf( "error max = %8g\t" "avg = %8g\t" "|avg| = %8g\t" "to %8g\n",
                    max_error, cum_error / n, total_error / n, _mm_cvtss_f32( arg ) );
    } while ( n == bucket_size );
}

int main() {
    std::printf( "4 insn x^12/5:\n" );
    test_pow( 12./5, & fastpow< 12, 5, 1059, 1000 > );
    std::printf( "14 insn x^12/5:\n" );
    test_pow( 12./5, & pow125_4 );
    std::printf( "6 insn x^5/12:\n" );
    test_pow( 5./12, & pow512_2 );
    std::printf( "14 insn x^5/12:\n" );
    test_pow( 5./12, & pow512_4 );
}

Sortie:

4 insn x^12/5:
Domain from 1.36909e-23
error max =      inf    avg =      inf  |avg| =      inf    to 8.97249e-19
error max =  2267.14    avg =  139.175  |avg| =  139.193    to 5.88021e-14
error max = 0.123606    avg = -0.000102963  |avg| = 0.0371122   to 3.85365e-09
error max = 0.123607    avg = -0.000108978  |avg| = 0.0368548   to 0.000252553
error max =  0.12361    avg = 7.28909e-05   |avg| = 0.037507    to  16.5513
error max = 0.123612    avg = -0.000258619  |avg| = 0.0365618   to 1.08471e+06
error max = 0.123611    avg = 8.70966e-05   |avg| = 0.0374369   to 7.10874e+10
error max =  0.12361    avg = -0.000103047  |avg| = 0.0371122   to 4.65878e+15
error max = 0.123609    avg =      nan  |avg| =      nan    to 1.16469e+16
14 insn x^12/5:
Domain from 1.42795e-19
error max =      inf    avg =      nan  |avg| =      nan    to 9.35823e-15
error max = 0.000936462 avg = 2.0202e-05    |avg| = 0.000133764 to 6.13301e-10
error max = 0.000792752 avg = 1.45717e-05   |avg| = 0.000129936 to 4.01933e-05
error max = 0.000791785 avg = 7.0132e-06    |avg| = 0.000129923 to  2.63411
error max = 0.000787589 avg = 1.20745e-05   |avg| = 0.000129347 to   172629
error max = 0.000786553 avg = 1.62351e-05   |avg| = 0.000132397 to 1.13134e+10
error max = 0.000785586 avg = 8.25205e-06   |avg| = 0.00013037  to 6.98147e+12
6 insn x^5/12:
Domain from 9.86076e-32
error max = 0.0284339   avg = 0.000441158   |avg| = 0.00967327  to 6.46235e-27
error max = 0.0284342   avg = -5.79938e-06  |avg| = 0.00897913  to 4.23516e-22
error max = 0.0284341   avg = -0.000140706  |avg| = 0.00897084  to 2.77556e-17
error max = 0.028434    avg = 0.000440504   |avg| = 0.00967325  to 1.81899e-12
error max = 0.0284339   avg = -6.11153e-06  |avg| = 0.00897915  to 1.19209e-07
error max = 0.0284298   avg = -0.000140597  |avg| = 0.00897084  to 0.0078125
error max = 0.0284371   avg = 0.000439748   |avg| = 0.00967319  to      512
error max = 0.028437    avg = -7.74294e-06  |avg| = 0.00897924  to 3.35544e+07
error max = 0.0284369   avg = -0.000142036  |avg| = 0.00897089  to 2.19902e+12
error max = 0.0284368   avg = 0.000439183   |avg| = 0.0096732   to 1.44115e+17
error max = 0.0284367   avg = -7.41244e-06  |avg| = 0.00897923  to 9.44473e+21
error max = 0.0284366   avg = -0.000141706  |avg| = 0.00897088  to 6.1897e+26
error max = 0.485129    avg = -0.0401671    |avg| = 0.048422    to 4.05648e+31
error max = 0.994932    avg = -0.891494 |avg| = 0.891494    to 2.65846e+36
error max = 0.999329    avg =      nan  |avg| =      nan    to       -0
14 insn x^5/12:
Domain from 2.64698e-23
error max =  0.13556    avg = 0.00125936    |avg| = 0.00354677  to 1.73472e-18
error max = 0.000564988 avg = 2.51458e-06   |avg| = 0.000113709 to 1.13687e-13
error max = 0.000565065 avg = -1.49258e-06  |avg| = 0.000112553 to 7.45058e-09
error max = 0.000565143 avg = 1.5293e-06    |avg| = 0.000112864 to 0.000488281
error max = 0.000565298 avg = 2.76457e-06   |avg| = 0.000113713 to       32
error max = 0.000565453 avg = -1.61276e-06  |avg| = 0.000112561 to 2.09715e+06
error max = 0.000565531 avg = 1.42628e-06   |avg| = 0.000112866 to 1.37439e+11
error max = 0.000565686 avg = 2.71505e-06   |avg| = 0.000113715 to 9.0072e+15
error max = 0.000565763 avg = -1.56586e-06  |avg| = 0.000112415 to 1.84467e+19

Je soupçonne que la précision du 5/12 plus précis est limitée par l'opération rsqrt.

22
Potatoswatter

Une autre réponse parce que cela est très différent de ma réponse précédente et que c’est extrêmement rapide. L'erreur relative est 3e-8. Vous voulez plus de précision? Ajoutez quelques termes de Chebychev supplémentaires. Il est préférable de garder l'ordre impair car cela crée une petite discontinuité entre 2 ^ n-epsilon et 2 ^ n + epsilon.

#include <stdlib.h>
#include <math.h>

// Returns x^(5/12) for x in [1,2), to within 3e-8 (relative error).
// Want more precision? Add more Chebychev polynomial coefs.
double pow512norm (
   double x)
{
   static const int N = 8;

   // Chebychev polynomial terms.
   // Non-zero terms calculated via
   //   integrate (2/pi)*ChebyshevT[n,u]/sqrt(1-u^2)*((u+3)/2)^(5/12)
   //   from -1 to 1
   // Zeroth term is similar except it uses 1/pi rather than 2/pi.
   static const double Cn[N] = { 
       1.1758200232996901923,
       0.16665763094889061230,
      -0.0083154894939042125035,
       0.00075187976780420279038,
      // Wolfram alpha doesn't want to compute the remaining terms
      // to more precision (it times out).
      -0.0000832402,
       0.0000102292,
      -1.3401e-6,
       1.83334e-7};

   double Tn[N];

   double u = 2.0*x - 3.0;

   Tn[0] = 1.0;
   Tn[1] = u;
   for (int ii = 2; ii < N; ++ii) {
      Tn[ii] = 2*u*Tn[ii-1] - Tn[ii-2];
   }   

   double y = 0.0;
   for (int ii = N-1; ii >= 0; --ii) {
      y += Cn[ii]*Tn[ii];
   }   

   return y;
}


// Returns x^(5/12) to within 3e-8 (relative error).
double pow512 (
   double x)
{
   static const double pow2_512[12] = {
      1.0,
      pow(2.0, 5.0/12.0),
      pow(4.0, 5.0/12.0),
      pow(8.0, 5.0/12.0),
      pow(16.0, 5.0/12.0),
      pow(32.0, 5.0/12.0),
      pow(64.0, 5.0/12.0),
      pow(128.0, 5.0/12.0),
      pow(256.0, 5.0/12.0),
      pow(512.0, 5.0/12.0),
      pow(1024.0, 5.0/12.0),
      pow(2048.0, 5.0/12.0)
   };

   double s;
   int iexp;

   s = frexp (x, &iexp);
   s *= 2.0;
   iexp -= 1;

   div_t qr = div (iexp, 12);
   if (qr.rem < 0) {
      qr.quot -= 1;
      qr.rem += 12;
   }

   return ldexp (pow512norm(s)*pow2_512[qr.rem], 5*qr.quot);
}

Addendum: Qu'est-ce qui se passe ici?
À la demande, ce qui suit explique le fonctionnement du code ci-dessus.

Vue d'ensemble
Le code ci-dessus définit deux fonctions, double pow512norm (double x) et double pow512 (double x). Ce dernier est le point d’entrée dans la suite; c'est la fonction que le code utilisateur devrait appeler pour calculer x ^ (5/12). La fonction pow512norm(x) utilise les polynômes de Chebyshev pour approcher x ^ (5/12), mais uniquement pour x dans l'intervalle [1,2]. (Utilisez pow512norm(x) pour les valeurs de x situées en dehors de cette plage et le résultat sera illisible.)

La fonction pow512(x) divise la x entrante en une paire (double s, int n) telle que x = s * 2^n et telle que 1≤s <2. Un partitionnement supplémentaire de n en (int q, unsigned int r) tel que n = 12*q + r et r est inférieur à 12, me permet de scinder le problème de la recherche de x ^ (5/12) en plusieurs parties:

  1. x^(5/12)=(s^(5/12))*((2^n)^(5/12)) via (u v) ^ a = (u ^ a)} (v ^ a) pour positif u, v et réel a.
  2. s^(5/12) est calculé via pow512norm(s).
  3. (2^n)^(5/12)=(2^(12*q+r))^(5/12) via substitution.
  4. 2^(12*q+r)=(2^(12*q))*(2^r) via u^(a+b)=(u^a)*(u^b) pour u positif, réel a, b.
  5. (2^(12*q+r))^(5/12)=(2^(5*q))*((2^r)^(5/12)) via quelques manipulations supplémentaires.
  6. (2^r)^(5/12) est calculé par la table de recherche pow2_512.
  7. Calculez pow512norm(s)*pow2_512[qr.rem] et nous y sommes presque. qr.rem est ici la valeur r calculée à l'étape 3 ci-dessus. Tout ce qui est nécessaire est de le multiplier par 2^(5*q) pour obtenir le résultat souhaité.
  8. C'est exactement ce que fait la fonction de bibliothèque mathématique ldexp.

Approximation de fonction
Le but ici est de proposer une approximation facilement calculable de f (x) = x ^ (5/12) qui soit «assez bonne» pour le problème à résoudre. Notre approximation devrait être proche de f(x) dans un sens. Question rhétorique: que veut dire «proche de»? Deux interprétations concurrentes minimisent l’erreur quadratique moyenne par rapport à l’erreur absolue maximale.

Je vais utiliser une analogie du marché boursier pour décrire la différence entre ceux-ci. Supposons que vous souhaitiez économiser pour votre retraite éventuelle. Si vous êtes dans la vingtaine, la meilleure chose à faire est d'investir dans des actions ou des fonds de marché boursier. En effet, sur une période assez longue, le marché boursier bat en moyenne tous les autres projets d’investissement. Cependant, nous avons tous vu des moments où placer de l'argent dans des actions est une très mauvaise chose à faire. Si vous êtes dans la cinquantaine ou la soixantaine (ou dans la quarantaine si vous voulez prendre votre retraite à un jeune âge), vous devez investir un peu plus prudemment. Ces ralentissements peuvent avoir sur votre portefeuille de retraite.

Retour à l'approximation des fonctions: En tant qu'utilisateur d'une approximation, vous vous inquiétez généralement de l'erreur dans le pire des cas plutôt que de la performance "en moyenne". Utilisez une approximation construite pour donner la meilleure performance "en moyenne" (par exemple, les moindres carrés) et la loi de Murphy dicte à votre programme de passer beaucoup de temps à utiliser cette approximation exactement là où la performance est bien pire que la moyenne. Ce que vous voulez, c'est une approximation minimax, quelque chose qui minimise l'erreur absolue maximale sur un domaine. Une bonne bibliothèque de mathématiques adoptera une approche minimax plutôt que la méthode des moindres carrés, car cela permettra aux auteurs de la bibliothèque de mathématiques de garantir les performances de leur bibliothèque.

Les bibliothèques mathématiques utilisent généralement un polynôme ou un polynôme rationnel pour approcher une fonction f(x) sur un domaine a≤x≤b. Supposons que la fonction f(x) est analytique sur ce domaine et que vous souhaitez approximer la fonction par un polynôme p(x) de degré N. Pour un degré N donné, il existe des polynôme unique et magique p(x) tel que p(x)-f(x) possède N + 2 extrema sur [a, b] et tel que les valeurs absolues de ces N + 2 extrema sont tous égaux. Trouver ce polynôme magique p(x) est le Saint Graal des approximateurs de fonction.

Je n'ai pas trouvé ce Saint Graal pour vous. J'ai plutôt utilisé une approximation de Chebyshev. Les polynômes de Chebyshev du premier type sont un ensemble de polynômes orthogonaux (mais non orthonormaux) dotés de très jolies caractéristiques en termes d'approximation de fonctions. L’approximation de Chebyshev est souvent très proche de ce polynôme magique p (x). (En fait, l'algorithme d'échange de Remez qui constate que le polynôme du Saint-Graal commence généralement par une approximation de Chebyshev.)

pow512norm (x)
Cette fonction utilise l'approximation de Chebyshev pour trouver un polynôme p * (x) qui se rapproche de x ^ (5/12). Ici, j'utilise p * (x) pour distinguer cette approximation de Chebyshev du polynôme magique p(x) décrit ci-dessus. L'approximation de Chebyshev p * (x) est facile à trouver; trouver p(x) est un ours. L'approximation de Chebyshev p * (x) est sum_i Cn [i] * Tn (i, x), où Cn [i] sont les coefficients de Chebyshev et Tn (i, x) sont les polynômes de Chebyshev évalués à x.

J'ai utilisé Wolfram alpha pour trouver les coefficients de Chebyshev Cn pour moi. Par exemple, ceci calcule Cn[1] . La première zone après la zone de saisie contient la réponse souhaitée, à savoir 0.166658 dans ce cas. Ce n'est pas autant de chiffres que je voudrais. Cliquez sur 'plus de chiffres' et le tour est joué, vous obtenez beaucoup plus de chiffres. Wolfram alpha est gratuit; il y a une limite sur la quantité de calcul que cela va faire. Il frappe cette limite sur des termes d'ordre supérieur. (Si vous achetez ou avez accès à mathematica, vous serez en mesure de calculer ces coefficients d'ordre élevé avec un degré de précision élevé.)

Les polynômes de Chebyshev Tn (x) sont calculés dans le tableau Tn. En plus de donner quelque chose de très proche du polynôme magique p (x), une autre raison d'utiliser l'approximation de Chebyshev est que les valeurs de ces polynômes de Chebyshev sont faciles à calculer: Commencez par Tn[0]=1 et Tn[1]=x, puis calculez de manière itérative Tn[i]=2*x*Tn[i-1] - Tn[i-2]. (J'ai utilisé 'ii' comme variable d'indexation plutôt que 'i' dans mon code. Je n'ai jamais utilisé 'i' comme nom de variable. Combien de mots dans la langue anglaise ont un 'i' dans le mot? Combien en ont deux consécutives 'i's?)

pow512 (x)
pow512 est la fonction que le code utilisateur devrait appeler. J'ai déjà décrit les bases de cette fonction ci-dessus. Quelques informations supplémentaires: La fonction de bibliothèque mathématique frexp(x) renvoie le significande s et l’exposant iexp pour l’entrée x. (Problème mineur: je veux s entre 1 et 2 pour être utilisé avec pow512norm mais frexp renvoie une valeur comprise entre 0,5 et 1.) La fonction de bibliothèque mathématique div renvoie le quotient et le reste pour la division entière dans un swop foop. Enfin, j'utilise la fonction de bibliothèque mathématique ldexp pour assembler les trois parties afin de former la réponse finale.

32
David Hammen

Ian Stephenson a écrit ce code dont il prétend qu'il surpasse pow(). Il décrit l'idée comme suit:

Pow est essentiellement implémenté à l'aide de Log's: pow(a,b)=x(logx(a)*b). donc nous avons besoin d’un journal rapide et d’un exposant rapide - peu importe la nature de x, nous utilisons donc 2. Le truc, c’est que la virgule flottante numéro est déjà dans un style de journal format:

a=M*2E

Prendre le journal des deux côtés donne:

log2(a)=log2(M)+E

ou plus simplement:

log2(a)~=E

En d’autres termes, si nous prenons la représentation À virgule flottante d’un nombre et que Extrayons l’exposant, nous avons Quelque chose qui constitue un bon point de départ . bûche. Il s'avère que lorsque nous Faisons cela en massant les motifs de bits, La Mantisse finit par donner une bonne Approximation de l'erreur, et cela Fonctionne plutôt bien. bien.

Cela devrait suffire pour de simples calculs D’éclairage, mais si vous avez besoin de Quelque chose de mieux, vous pouvez alors extraire La Mantisse et l’utiliser pour Calculer une facteur de correction quadratique qui est assez précis.

20
PengOne

Tout d'abord, l'utilisation de flotteurs ne va pas acheter beaucoup sur la plupart des machines de nos jours. En fait, les doubles peuvent être plus rapides. Votre puissance, 1.0/2.4, est 5/12 ou 1/3 * (1 + 1/4). Même si cela appelle cbrt (une fois) et sqrt (deux fois!), Il est toujours deux fois plus rapide que d’utiliser pow (). (Optimisation: -O3, compilateur: i686-Apple-darwin10-g ++ - 4.2.1). 

#include <math.h> // cmath does not provide cbrt; C99 does.
double xpow512 (double x) {
  double cbrtx = cbrt(x);
  return cbrtx*sqrt(sqrt(cbrtx));
}
16
David Hammen

Cela pourrait ne pas répondre à votre question.

Les 2.4f et 1/2.4f me rendent très méfiant, car ce sont exactement les pouvoirs utilisés pour la conversion entre sRGB et un espace colorimétrique RVB linéaire. Donc, vous essayez peut-être d'optimiser cela, spécifiquement. Je ne sais pas, c'est pourquoi cela pourrait ne pas répondre à votre question.

Si tel est le cas, essayez d'utiliser une table de recherche. Quelque chose comme:

__attribute__((aligned(64))
static const unsigned short SRGB_TO_LINEAR[256] = { ... };
__attribute__((aligned(64))
static const unsigned short LINEAR_TO_SRGB[256] = { ... };

void apply_lut(const unsigned short lut[256], unsigned char *src, ...

Si vous utilisez des données 16 bits, modifiez-les selon vos besoins. De toute façon, je ferais la table sur 16 bits afin que vous puissiez tramer le résultat si nécessaire lorsque vous travaillez avec des données 8 bits. Cela ne fonctionnera évidemment pas très bien si vos données sont en virgule flottante pour commencer - mais il n’a pas vraiment de sens de stocker les données sRGB en virgule flottante, vous pouvez donc aussi convertir en 16 bits/8 bits en premier. puis effectuez la conversion de linéaire en sRGB.

(La raison pour laquelle sRVB n'a pas de sens car la virgule flottante est que le HDR doit être linéaire et que sRVB n'est pratique que pour le stockage sur disque ou l'affichage à l'écran, mais pas pour la manipulation.)

15
Dietrich Epp

La série binomiale représente un exposant constant, mais vous ne pourrez l'utiliser que si vous pouvez normaliser toutes vos entrées dans l'intervalle [1,2). (Notez qu'il calcule (1 + x) ^ a). Vous devrez faire une analyse pour déterminer le nombre de termes dont vous avez besoin pour obtenir la précision souhaitée.

3
zvrba

Je vais répondre à la question que vous vraiment vouliez poser, à savoir comment effectuer une conversion sRGB <-> linéaire rapide. Pour faire cela avec précision et efficacité, nous pouvons utiliser des approximations polynomiales. Les approximations polynomiales suivantes ont été générées avec sollya et ont une erreur relative dans le pire des cas de 0,0144%.

inline double poly7(double x, double a, double b, double c, double d,
                              double e, double f, double g, double h) {
    double ab, cd, ef, gh, abcd, efgh, x2, x4;
    x2 = x*x; x4 = x2*x2;
    ab = a*x + b; cd = c*x + d;
    ef = e*x + f; gh = g*x + h;
    abcd = ab*x2 + cd; efgh = ef*x2 + gh;
    return abcd*x4 + efgh;
}

inline double srgb_to_linear(double x) {
    if (x <= 0.04045) return x / 12.92;

    // Polynomial approximation of ((x+0.055)/1.055)^2.4.
    return poly7(x, 0.15237971711927983387,
                   -0.57235993072870072762,
                    0.92097986411523535821,
                   -0.90208229831912012386,
                    0.88348956209696805075,
                    0.48110797889132134175,
                    0.03563925285274562038,
                    0.00084585397227064120);
}

inline double linear_to_srgb(double x) {
    if (x <= 0.0031308) return x * 12.92;

    // Piecewise polynomial approximation (divided by x^3)
    // of 1.055 * x^(1/2.4) - 0.055.
    if (x <= 0.0523) return poly7(x, -6681.49576364495442248881,
                                      1224.97114922729451791383,
                                      -100.23413743425112443219,
                                         6.60361150127077944916,
                                         0.06114808961060447245,
                                        -0.00022244138470139442,
                                         0.00000041231840827815,
                                        -0.00000000035133685895) / (x*x*x);

    return poly7(x, -0.18730034115395793881,
                     0.64677431008037400417,
                    -0.99032868647877825286,
                     1.20939072663263713636,
                     0.33433459165487383613,
                    -0.01345095746411287783,
                     0.00044351684288719036,
                    -0.00000664263587520855) / (x*x*x);
}

Et l’entrée sollya utilisée pour générer les polynômes:

suppressmessage(174);
f = ((x+0.055)/1.055)^2.4;
p0 = fpminimax(f, 7, [|D...|], [0.04045;1], relative);
p = fpminimax(f/(p0(1)+1e-18), 7, [|D...|], [0.04045;1], relative);
print("relative:", dirtyinfnorm((f-p)/f, [s;1]));
print("absolute:", dirtyinfnorm((f-p), [s;1]));
print(canonical(p));

s = 0.0523;
z = 3;
f = 1.055 * x^(1/2.4) - 0.055;

p = fpminimax(1.055 * (x^(z+1/2.4) - 0.055*x^z/1.055), 7, [|D...|], [0.0031308;s], relative)/x^z;
print("relative:", dirtyinfnorm((f-p)/f, [0.0031308;s]));
print("absolute:", dirtyinfnorm((f-p), [0.0031308;s]));
print(canonical(p));

p = fpminimax(1.055 * (x^(z+1/2.4) - 0.055*x^z/1.055), 7, [|D...|], [s;1], relative)/x^z;
print("relative:", dirtyinfnorm((f-p)/f, [s;1]));
print("absolute:", dirtyinfnorm((f-p), [s;1]));
print(canonical(p));
2
orlp

Voici une idée que vous pouvez utiliser avec l’une des méthodes de calcul rapides. Que cela accélère les choses dépend de la manière dont vos données arrivent. Vous pouvez utiliser le fait que si vous connaissez x et pow(x, n), vous pouvez utiliser le taux de changement du pouvoir pour calculer une approximation raisonnable de pow(x + delta, n) pour un petit delta, avec une seule multiplication et addition (plus ou moins). Si les valeurs successives que vous alimentez dans vos fonctions d'alimentation sont assez proches les unes des autres, le coût total du calcul précis sur plusieurs appels de fonction sera amorti. Notez que vous n'avez pas besoin d'un calcul supplémentaire de la quantité de poudre pour obtenir le dérivé. Vous pouvez étendre cela à la dérivée seconde pour pouvoir utiliser un quadratique, ce qui augmenterait la delta que vous pourriez utiliser tout en obtenant la même précision.

1
Permaquid

Pour les exposants de 2.4, vous pouvez créer une table de correspondance pour toutes vos valeurs de 2.4 et votre valeur de référence ou peut-être une fonction d'ordre supérieur pour renseigner les valeurs entre-deux si la table n'est pas assez précise (en gros, une table de journal énorme).

Ou valeur carré * valeur aux 2/5s qui pourraient prendre la valeur initiale du carré de la première moitié de la fonction puis la 5ème racine. Pour la cinquième racine, vous pouvez utiliser Newton ou faire un autre approximateur rapide, même si, honnêtement, une fois que vous en êtes arrivé à ce point, vous feriez probablement mieux de vous contenter des fonctions exp et log avec les fonctions de série abrégées appropriées.

1
Michael Dorgan

Donc, traditionnellement, la fonction powf(x, p) = x^p est résolue en réécrivant x en tant que x=2^(log2(x)) making powf(x,p) = 2^(p*log2(x)), ce qui transforme le problème en deux approximations exp2() & log2(). Cela présente l’avantage de fonctionner avec de plus grandes puissances p, mais l’inconvénient est que ce n’est pas la solution optimale pour une puissance constante p et pour une entrée spécifiée 0 ≤ x ≤ 1.

Lorsque la puissance p > 1, la réponse est un polynôme minimax trivial sur le 0 ≤ x ≤ 1 lié, ce qui est le cas pour p = 12/5 = 2.4 comme on peut le voir ci-dessous:

float pow12_5(float x){
    float mp;
    // Minimax horner polynomials for x^(5/12), Note: choose the accurarcy required then implement with fma() [Fused Multiply Accumulates]
    // mp = 0x4.a84a38p-12 + x * (-0xd.e5648p-8 + x * (0xa.d82fep-4 + x * 0x6.062668p-4)); // 1.13705697e-3
    mp = 0x1.117542p-12 + x * (-0x5.91e6ap-8 + x * (0x8.0f50ep-4 + x * (0xa.aa231p-4 + x * (-0x2.62787p-4))));  // 2.6079002e-4
    // mp = 0x5.a522ap-16 + x * (-0x2.d997fcp-8 + x * (0x6.8f6d1p-4 + x * (0xf.21285p-4 + x * (-0x7.b5b248p-4 + x * 0x2.32b668p-4))));  // 8.61377e-5
    // mp = 0x2.4f5538p-16 + x * (-0x1.abcdecp-8 + x * (0x5.97464p-4 + x * (0x1.399edap0 + x * (-0x1.0d363ap0 + x * (0xa.a54a3p-4 + x * (-0x2.e8a77cp-4))))));  // 3.524655e-5
    return(mp);
}

Cependant, lorsque p < 1, l'approximation minimax sur le 0 ≤ x ≤ 1 lié ne converge pas correctement vers la précision souhaitée. Une option [pas vraiment] est de réécrire le problème y=x^p=x^(p+m)/x^mm=1,2,3 est un entier positif, rendant la nouvelle approximation de puissance p > 1 mais introduisant une division qui est par nature plus lente. 

Il existe cependant une autre option qui consiste à décomposer l’entrée x en exposant à virgule flottante et sous forme de mantisse: 

x = mx* 2^(ex) where 1 ≤ mx < 2
y = x^(5/12) = mx^(5/12) * 2^((5/12)*ex), let ey = floor(5*ex/12), k = (5*ex) % 12
  = mx^(5/12) * 2^(k/12) * 2^(ey)

L’approximation minimax de mx^(5/12) sur 1 ≤ mx < 2 converge maintenant beaucoup plus rapidement qu’avant, sans division, mais nécessite une table d’essai à 12 points pour la fonction 2^(k/12). Le code est ci-dessous:

float powk_12LUT[] = {0x1.0p0, 0x1.0f38fap0, 0x1.1f59acp0,  0x1.306fep0, 0x1.428a3p0, 0x1.55b81p0, 0x1.6a09e6p0, 0x1.7f910ep0, 0x1.965feap0, 0x1.ae89fap0, 0x1.c823ep0, 0x1.e3437ep0};
float pow5_12(float x){
    union{float f; uint32_t u;} v, e2;
    float poff, m, e, ei;
    int xe;

    v.f = x;
    xe = ((v.u >> 23) - 127);

    if(xe < -127) return(0.0f);

    // Calculate remainder k in 2^(k/12) to find LUT
    e = xe * (5.0f/12.0f);
    ei = floorf(e);
    poff = powk_12LUT[(int)(12.0f * (e - ei))];

    e2.u = ((int)ei + 127) << 23;   // Calculate the exponent
    v.u = (v.u & ~(0xFFuL << 23)) | (0x7FuL << 23); // Normalize exponent to zero

    // Approximate mx^(5/12) on [1,2), with appropriate degree minimax
    // m = 0x8.87592p-4 + v.f * (0x8.8f056p-4 + v.f * (-0x1.134044p-4));    // 7.6125e-4
    // m = 0x7.582138p-4 + v.f * (0xb.1666bp-4 + v.f * (-0x2.d21954p-4 + v.f * 0x6.3ea0cp-8));  // 8.4522726e-5
    m = 0x6.9465cp-4 + v.f * (0xd.43015p-4 + v.f * (-0x5.17b2a8p-4 + v.f * (0x1.6cb1f8p-4 + v.f * (-0x2.c5b76p-8))));   // 1.04091259e-5
    // m = 0x6.08242p-4 + v.f * (0xf.352bdp-4 + v.f * (-0x7.d0c1bp-4 + v.f * (0x3.4d153p-4 + v.f * (-0xc.f7a42p-8 + v.f * 0x1.5d840cp-8))));    // 1.367401e-6

    return(m * poff * e2.f);
}
0
nimig18