¿Optimizaciones para pow () con un exponente const no entero?


Tengo puntos calientes en mi código donde estoy haciendo pow() ocupando alrededor del 10-20% de mi tiempo de ejecución.

Mi entrada a pow(x,y) es muy específica, así que me pregunto si hay una manera de rodar dos pow() aproximaciones (una para cada exponente) con mayor rendimiento:

  • Tengo dos exponentes constantes: 2.4 y 1/2.4.
  • Cuando el exponente es 2.4, x estará en el rango (0.090473935, 1.0].
  • Cuando el exponente es 1/2. 4, x estará en el rango (0.0031308, 1.0].
  • Estoy usando vectores SSE/AVX float. Si se pueden aprovechar los detalles de la plataforma, ¡adelante!

Una tasa de error máxima de alrededor del 0.01% es ideal, aunque también estoy interesado en algoritmos de precisión completa (para float).

Ya estoy usando un ayuno pow() aproximación , pero no tiene en cuenta estas restricciones. ¿Es posible hacerlo mejor?

Author: Cory Nelson, 2011-06-25

10 answers

En la vena de hacking IEEE 754, aquí hay otra solución que es más rápida y menos "mágica"."Se logra un margen de error de .08% en aproximadamente una docena de ciclos de reloj (para el caso de p=2.4, en una CPU Intel Merom).

Los números de coma flotante se inventaron originalmente como una aproximación a los logaritmos, por lo que puede usar el valor entero como una aproximación de log2. Esto es algo realizable aplicando la instrucción convert-from-integer a un valor de coma flotante, para obtener otro valor de coma flotante.

Para completar el cálculo pow, puede multiplicar por un factor constante y convertir el logaritmo de nuevo con la instrucción convert-to-integer. En la ESS, las instrucciones pertinentes son cvtdq2ps y cvtps2dq.

No es tan simple, sin embargo. El campo exponente en IEEE 754 está firmado, con un valor de sesgo de 127 que representa un exponente de cero. Este sesgo debe eliminarse antes de multiplicar el logaritmo y volver a agregarse antes de exponenciar. Además, el ajuste de sesgo por resta no funcionará en cero. Afortunadamente, ambos ajustes se pueden lograr multiplicando por un factor constante de antemano.

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 ) es el factor constante. Esta función es bastante especializada: no funcionará con exponentes fraccionales pequeños, porque el factor constante crece exponencialmente con el inverso del exponente y se desbordará. No funcionará con exponentes negativos. Los exponentes grandes conducen a un error alto, porque los bits de mantisa son mezclado con los bits exponentes por la multiplicación.

Pero, es solo 4 instrucciones rápidas de largo. Pre-multiplicar, convertir de " entero "(a logaritmo), poder-multiplicar, convertir a" entero " (de logaritmo). Las conversiones son muy rápidas en esta implementación de SSE. También podemos exprimir un coeficiente constante extra en la primera multiplicación.

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;
}

Unos pocos ensayos con exponente = 2.4 muestran que esto se sobrestima consistentemente en aproximadamente un 5%. (La rutina siempre está garantizada sobreestimar.) Simplemente podría multiplicar por 0.95, pero unas pocas instrucciones más nos darán unos 4 dígitos decimales de precisión, que deberían ser suficientes para los gráficos.

La clave es hacer coincidir la sobreestimación con una subestimación, y tomar el promedio.

  • Calcular x^0.8: cuatro instrucciones, error ~ +3%.
  • Calcular x^-0.4: uno rsqrtps. (Esto es bastante preciso, pero sacrifica la capacidad de trabajar con cero.)
  • Calcular x^0.4: uno mulps.
  • Calcular x^-0.2: uno rsqrtps.
  • Calcular x^2: uno mulps.
  • Calcular x^3: uno mulps.
  • x^2.4 = x^2 * x^0.4: uno mulps. Esta es la sobreestimación.
  • x^2.4 = x^3 * x^-0.4 * x^-0.2: dos mulps. Esta es la subestimación.
  • Promedia lo anterior: uno addps, uno mulps.

Recuento de instrucciones: catorce, incluidas dos conversiones con latencia = 5 y dos estimaciones de raíz cuadrada recíproca con rendimiento = 4.

Para tomar correctamente el promedio, queremos ponderar las estimaciones por sus errores esperados. La subestimación eleva el error a una potencia de 0.6 vs 0.4, por lo que esperamos que sea 1.5 x como erróneo. La ponderación no agrega ninguna instrucción; se puede hacer en el pre-factor. Llamar al coeficiente a: a^0.5 = 1.5 a^-0.75, y = 1.38316186.

El error final es sobre .015%, o 2 órdenes de magnitud mejor que el resultado inicial fastpow. El tiempo de ejecución es de aproximadamente una docena de ciclos para un bucle ocupado con variables de origen y destino volatile although aunque se superpone a las iteraciones, el uso en el mundo real también verá paralelismo a nivel de instrucción. Teniendo en cuenta SIMD, que es un rendimiento de un resultado escalar por 3 ciclos!

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 );
}

Bueno sorry siento no haber podido publicar esto antes. Y extenderlo a x^1/2. 4 se deja como un ejercicio; v).


Actualización con estadísticas

He implementado un pequeño instrumento de prueba y dos x(512) casos correspondientes a lo anterior.

#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 );
}

Salida:

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

Sospecho que la precisión del 5/12 más preciso está siendo limitada por la operación rsqrt.

 22
Author: Potatoswatter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-06 01:22:44

Otra respuesta porque esto es muy diferente de mi respuesta anterior, y esto es muy rápido. El error relativo es 3e-8. Quieres más precisión? Añade un par de términos más de Chebychev. Es mejor mantener el orden impar ya que esto hace que para una pequeña discontinuidad entre 2^n-epsilon y 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);
}

Anexo: ¿Qué está pasando aquí?
Por solicitud, lo siguiente explica cómo funciona el código anterior.

Sinopsis
El código anterior define dos funciones, double pow512norm (double x) y double pow512 (double x). Este último es el punto de entrada a la suite; esta es la función que el código de usuario debe llamar para calcular x^(5/12). La función pow512norm(x) utiliza polinomios de Chebyshev para aproximar x^(5/12), pero solo para x en el rango [1,2]. (Use pow512norm(x) para valores de x fuera de ese rango y el resultado será basura.)

La función pow512(x) divide el entrante x en un par (double s, int n) tal que x = s * 2^n y tal que 1≤sn en (int q, unsigned int r) tal que n = 12*q + r y r es menor que 12 me permite dividir el problema de encontrar x^(5/12) en partes:

  1. x^(5/12)=(s^(5/12))*((2^n)^(5/12)) via (u v)^a=(u^a) (v^a) para u positivo,v y a real.
  2. s^(5/12) se calcula a través de pow512norm(s).
  3. (2^n)^(5/12)=(2^(12*q+r))^(5/12) mediante sustitución.
  4. 2^(12*q+r)=(2^(12*q))*(2^r) vía u^(a+b)=(u^a)*(u^b) para u positivo, a real, b.
  5. (2^(12*q+r))^(5/12)=(2^(5*q))*((2^r)^(5/12)) a través de algunas manipulaciones más.
  6. (2^r)^(5/12) se calcula mediante la tabla de búsqueda pow2_512.
  7. Calcular pow512norm(s)*pow2_512[qr.rem] y estamos casi allí. Aqui qr.rem es el valor r calculado en el paso 3 anterior. Todo lo que se necesita es multiplicar esto por 2^(5*q) para producir el resultado deseado.
  8. Eso es exactamente lo que hace la función de biblioteca matemática ldexp.

Aproximación De Funciones
El objetivo aquí es llegar a una aproximación fácilmente computable de f(x) = x^(5/12) que es 'lo suficientemente bueno' para el problema en cuestión. Nuestra aproximación debe estar cerca de f (x) en algún sentido. Pregunta retórica: ¿Qué hace 'cerca de' ¿mala? Dos interpretaciones competidoras son minimizar el error cuadrado medio versus minimizar el error absoluto máximo.

Voy a utilizar una analogía del mercado de valores para describir la diferencia entre estos. Supongamos que desea ahorrar para su eventual jubilación. Si usted está en sus veinte años, lo mejor que puede hacer es invertir en acciones o fondos del mercado de valores. Esto se debe a que durante un período de tiempo lo suficientemente largo, el mercado de valores en promedio supera a cualquier otro esquema de inversión. Sin embargo, todos hemos visto veces cuando poner dinero en acciones es una cosa muy mala que hacer. Si usted está en sus cincuenta o sesenta (o cuarenta años si desea retirarse joven) que necesita para invertir un poco más conservador. Esas bajadas pueden tener en tu cartera de jubilación.

Volver a la aproximación de funciones: Como consumidor de alguna aproximación, normalmente le preocupa el error en el peor de los casos en lugar del rendimiento "en promedio". Utilice alguna aproximación construida para dar el mejor rendimiento " en promedio " (por ejemplo, mínimos cuadrados) y la ley de Murphy dicta que su programa pasará mucho tiempo usando la aproximación exactamente donde el rendimiento es mucho peor que el promedio. Lo que quieres es una aproximación minimax, algo que minimice el error absoluto máximo sobre algún dominio. Una buena biblioteca de matemáticas tomará un enfoque minimax en lugar de un enfoque de mínimos cuadrados porque esto permite a los autores de la biblioteca de matemáticas dar un rendimiento garantizado de su biblioteca.

Las bibliotecas matemáticas típicamente usan un polinomio o un polinomio racional para aproximar alguna función f(x) sobre algún dominio a≤x≤b. Supongamos que la función f(x) es analítica sobre este dominio y desea aproximar la función por algún polinomio p(x) de grado N. Para un grado N dado existe algún polinomio mágico y único p(x) tal que p(x)-f(x) tiene N+2 extremos sobre [a, b] y tal que los valores absolutos de estos N+2 extremos son todos iguales entre sí. Encontrar esto el polinomio mágico p (x) es el santo grial de los aproximadores de función.

No encontré ese santo grial para ti. En su lugar, utilicé una aproximación de Chebyshev. Los polinomios de Chebyshev del primer tipo son un conjunto ortogonal (pero no ortonormal) de polinomios con algunas características muy agradables cuando se trata de la aproximación de funciones. La aproximación de Chebyshev a menudo está muy cerca de ese polinomio mágico p (x). (De hecho, el algoritmo de intercambio Remez que encuentra ese santo grial el polinomio comienza típicamente con una aproximación de Chebyshev.)

Pow512norm(x)
Esta función utiliza la aproximación de Chebyshev para encontrar algún polinomio p*(x) que se aproxima a x^(5/12). Aquí estoy usando p * (x) para distinguir esta aproximación de Chebyshev del polinomio mágico p(x) descrito anteriormente. La aproximación de Chebyshev p*(x) es fácil de encontrar, encontrar p(x) es un oso. La aproximación de Chebyshev p * (x) es sum_i Cn [i] * Tn (i, x), donde los Cn [i] son los Chebyshev los coeficientes y Tn (i, x) son los polinomios de Chebyshev evaluados en x.

Usé Wolfram alpha para encontrar los coeficientes de Chebyshev Cn para mí. Por ejemplo, esto calcula Cn[1]. El primer cuadro después del cuadro de entrada tiene la respuesta deseada, 0.166658 en este caso. No son tantos dígitos como me gustaría. Haga clic en 'más dígitos' y listo, obtendrá muchos más dígitos. Wolfram alpha es gratis; hay un límite en la cantidad de computación que hará. Llega a ese límite en términos de orden más altos. (Si usted compra o tiene acceso a mathematica, podrá calcular esos coeficientes de alto orden con un alto grado de precisión.)

Los polinomios de Chebyshev Tn(x) se calculan en el array Tn. Más allá de dar algo muy cercano al polinomio mágico p(x), otra razón para usar la aproximación de Chebyshev es que los valores de esos polinomios de Chebyshev se calculan fácilmente: Comience con Tn[0]=1 y Tn[1]=x, y luego calcule iterativamente Tn[i]=2*x*Tn[i-1] - Tn[i-2]. (Yo usé 'ii' como variable de índice en lugar de' i ' en mi código. Nunca uso 'i' como nombre de variable. ¿Cuántas palabras en el idioma inglés tienen una " i " en la palabra? ¿Cuántos tienen dos i consecutivas?)

Pow512 (x)
pow512 es la función que el código de usuario debe llamar. Ya he descrito los conceptos básicos de esta función anteriormente. Algunos detalles más: La función math library frexp(x) devuelve el significante s y el exponente iexp para la entrada x. (Tema menor: quiero s entre 1 y 2 para usar con pow512norm pero frexp devuelve un valor entre 0.5 y 1.) La función math library div devuelve el cociente y el resto para la división de enteros en un pie de página hinchado. Finalmente, utilizo la función math library ldexp para juntar las tres partes para formar la respuesta final.

 32
Author: David Hammen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-26 14:28:29

Ian Stephenson escribió este código que él afirma supera pow(). Él describe la idea de la siguiente manera:

Pow se implementa básicamente usando log's: pow(a,b)=x(logx(a)*b). así que necesita un registro rápido y exponente rápido-it no importa lo que x es por lo que usamos 2. El truco es que un punto flotante el número ya está en un estilo de registro formato:

a=M*2E

Tomando el registro de ambos lados da:

log2(a)=log2(M)+E

O más simplemente:

log2(a)~=E

En otros palabras si tomamos la flotación representación puntual de un número, y extraer el Exponente que tenemos algo que es un buen punto de partida como su tronco. Resulta que cuando haga esto masajeando los patrones de bits, la Mantisa termina dando un buen aproximación al error, y funciona bastante bien.

Esto debería ser lo suficientemente bueno para simple cálculos de iluminación, pero si necesita algo mejor, a continuación, se puede extraer la Mantisa, y usa eso para calcular un factor de corrección cuadrática lo cual es bastante exacto.

 20
Author: PengOne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-25 02:29:50

En primer lugar, el uso de flotadores no va a comprar mucho en la mayoría de las máquinas de hoy en día. De hecho, los dobles pueden ser más rápidos. Su poder, 1.0 / 2.4, es 5/12 o 1/3*(1 + 1/4). A pesar de que esto está llamando cbrt (una vez) y sqrt (dos veces!) sigue siendo el doble de rápido que usando pow (). (Optimización: - O3, compilador: 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
Author: David Hammen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-25 02:57:58

Esto podría no responder a su pregunta.

El 2.4f y 1/2.4f me hacen sospechar mucho, porque esos son exactamente los poderes utilizados para convertir entre sRGB y un espacio de color RGB lineal. Por lo que en realidad podría estar tratando de optimizar que, específicamente. No lo sé, por lo que esto podría no responder a tu pregunta.

Si este es el caso, intente usar una tabla de búsqueda. Algo como:

__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 está utilizando datos de 16 bits, cambie según corresponda. Yo haría la tabla de 16 bits de todos modos para que pueda tramar el resultado si es necesario cuando se trabaja con datos de 8 bits. Esto obviamente no funcionará muy bien si sus datos son de punto flotante para empezar -- pero realmente no tiene sentido almacenar datos sRGB en punto flotante, por lo que también podría convertir a 16 bits / 8 bits primero y luego hacer la conversión de lineal a sRGB.

(La razón por la que sRGB no tiene sentido como punto flotante es que HDR debe ser lineal, y sRGB solo es conveniente para almacenar en disco o visualización en pantalla, pero no conveniente para la manipulación.)

 15
Author: Dietrich Epp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-25 03:22:36

La serie binomial da cuenta de un exponente constante, pero solo podrá usarlo si puede normalizar toda su entrada al rango [1,2). (Tenga en cuenta que calcula (1 + x)^a). Tendrá que hacer un análisis para decidir cuántos términos necesita para su precisión deseada.

 3
Author: zvrba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-25 05:49:44

Para exponentes de 2.4, puede hacer una tabla de búsqueda para todos sus valores de 2.4 y lirp o tal vez una función de orden superior para completar los valores intermedios si la tabla no era lo suficientemente precisa (básicamente una tabla de registro enorme.)

O, valor cuadrado * valor a los 2/5s que podría tomar el valor cuadrado inicial de la primera mitad de la función y luego la 5a raíz. Para la 5a raíz, podrías Newton o hacer algún otro aproximador rápido, aunque honestamente una vez que llegues a este punto, probablemente sea mejor que solo haga las funciones exp y log con las funciones de serie abreviada adecuadas usted mismo.

 1
Author: Michael Dorgan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-25 02:51:43

La siguiente es una idea que puede usar con cualquiera de los métodos de cálculo rápidos. Si ayuda a que las cosas vayan más rápido depende de cómo lleguen sus datos. Puedes usar el hecho de que si sabes x y pow(x, n), puedes usar la tasa de cambio de la potencia para calcular una aproximación razonable de pow(x + delta, n) para delta pequeños, con una sola multiplicación y suma (más o menos). Si los valores sucesivos que alimenta sus funciones de potencia están lo suficientemente cerca entre sí, esto amortizaría el costo total de la precisión cálculo sobre múltiples llamadas a funciones. Tenga en cuenta que no necesita un cálculo pow extra para obtener la derivada. Usted podría extender esto para usar la segunda derivada para que pueda usar una cuadrática, lo que aumentaría el delta que podría usar y aún así obtener la misma precisión.

 1
Author: Permaquid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-26 21:24:14

Voy a responder a la pregunta que realmente quería preguntar, que es cómo hacer una rápida conversión RGB lineal sRGB. Para hacer esto con precisión y eficiencia podemos usar aproximaciones polinómicas. Las siguientes aproximaciones polinómicas se han generado con sollya, y tienen un error relativo en el peor de los casos 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);
}

Y la entrada sollya utilizada para generar los polinomios:

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));
 1
Author: orlp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-23 03:27:22

Así que tradicionalmente el powf(x, p) = x^p se resuelve reescribiendo x como x=2^(log2(x)) haciendo powf(x,p) = 2^(p*log2(x)), lo que transforma el problema en dos aproximaciones exp2() & log2(). Esto tiene la ventaja de trabajar con potencias mayores p, sin embargo, la desventaja es que esta no es la solución óptima para una potencia constante p y sobre una entrada determinada 0 ≤ x ≤ 1.

Cuando la potencia p > 1, la respuesta es un polinomio minimax trivial sobre el límite 0 ≤ x ≤ 1, que es el caso de p = 12/5 = 2.4 como se puede ver abajo:

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);
}

Sin embargo, cuando p < 1 la aproximación de minimax sobre el límite 0 ≤ x ≤ 1 no converge apropiadamente a la precisión deseada. Una opción [no realmente] es reescribir el problema y=x^p=x^(p+m)/x^m donde m=1,2,3 es un entero positivo, haciendo la nueva aproximación de potencia p > 1 pero esto introduce la división que es inherentemente más lenta.

Sin embargo, hay otra opción que es descomponer la entrada x como su exponente de punto flotante y forma mantisa:

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)

La aproximación minimax de mx^(5/12) sobre 1 ≤ mx < 2 ahora converge mucho más rápido que antes, sin división, pero requiere LUT de 12 puntos para el 2^(k/12). El código es el siguiente:

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
Author: nimig18,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-17 21:33:54