Yo * no * quiera redondeo correcto para función exp

votos
10

El CCG aplicación de la C biblioteca matemática en Debian sistemas tiene aparentemente un (IEEE 754-2008) según norma ejecución de la función exp, lo que implica que el redondeo será siempre correcta:

( De Wikipedia ) El punto flotante IEEE garantías estándar que sumar, restar, multiplicar, dividir y multiplicar-fusionados añadir, raíz cuadrada, y el punto que queda flotando dará el resultado correctamente redondeado de la operación de precisión infinita. Sin esta garantía fue dada en la norma para las funciones más complejas 1985 y por lo general son sólo exacta a dentro del último bit en el mejor. Sin embargo, las 2008 garantías estándar que implementaciones conformes dará resultados correctamente redondeados que respeten el modo de redondeo activo; implementación de las funciones, sin embargo, es opcional.

Resulta que me encuentro con un caso en que esta función es en realidad obstaculiza, porque el resultado exacto de la expfunción es a menudo casi exactamente en el medio entre dos consecutivos doublevalores (1), y luego el programa lleva un montón de varios cálculos adicionales, perdiendo hasta un factor de 400 en velocidad (!): esto era en realidad la explicación a mi (mala preguntadas: -S) pregunta # 43530011 .

(1) Más precisamente, esto sucede cuando el argumento de expresulta ser de la forma (2 k + 1) × 2 -53 con k más bien un pequeño número entero (como 242 por ejemplo). En particular, los cálculos necesarios por pow (1. + x, 0.5)tienden a llamar expcon un argumento de este tipo, cuando xes del orden de magnitud de 2 -44 .

Desde implementaciones de redondeo correcta puede ser mucho tiempo en ciertas circunstancias, supongo que los desarrolladores también han ideado una manera de conseguir un resultado ligeramente menos precisa (por ejemplo, sólo hasta 0,6 ULP o algo así) en un tiempo que es (más o menos) acotada para cada valor del argumento en un rango dado ... (2)

… ¿¿Pero como hacer esto??

(2) Lo que quiero decir es que yo solo no quiero que algunos valores excepcionales del argumento como (2 k + 1) × 2 -53 serían mucho más tiempo que la mayoría de los valores del mismo orden de magnitud; pero por supuesto que no me importa si algunos valores excepcionales del argumento van mucho más rápido , o si grandes argumentos (en valor absoluto) necesitan un mayor tiempo de cálculo.

Aquí es un programa mínimo que muestra el fenómeno:

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

int main (void)
 {
  int i;
  double a, c;
  c = 0;
  clock_t start = clock ();
  for (i = 0; i < 1e6; ++i) // Doing a large number of times the same type of computation with different values, to smoothen random fluctuations.
   {
    a = (double) (1 + 2 * (rand () % 0x400)) / 0x20000000000000; // a has only a few significant digits, and its last non-zero digit is at (fixed-point) position 53.
    c += exp (a); // Just to be sure that the compiler will actually perform the computation of exp (a).
   }
  clock_t stop = clock ();
  printf (%e\n, c); // Just to be sure that the compiler will actually perform the computation.
  printf (Clock time spent: %d\n, stop - start);
  return 0;
 }

Ahora, después de gcc -std=c99 program53.c -lm -o program53:

$ ./program53
1.000000e+06
Clock time spent: 13470008
$ ./program53 
1.000000e+06
Clock time spent: 13292721
$ ./program53 
1.000000e+06
Clock time spent: 13201616

Por otra parte, con program52y program54(conseguimos mediante la sustitución 0x20000000000000por resp. 0x10000000000000Y 0x40000000000000):

$ ./program52
1.000000e+06
Clock time spent: 83594
$ ./program52
1.000000e+06
Clock time spent: 69095
$ ./program52
1.000000e+06
Clock time spent: 54694
$ ./program54
1.000000e+06
Clock time spent: 86151
$ ./program54
1.000000e+06
Clock time spent: 74209
$ ./program54
1.000000e+06
Clock time spent: 78612

Cuidado, el fenómeno es dependiente de la implementación! Al parecer, entre las implementaciones comunes, sólo los de los Debian sistemas (incluyendo Ubuntu ) muestran este fenómeno.

P.-S .: Espero que mi pregunta no es un duplicado: Busqué una pregunta similar a fondo sin éxito, pero tal vez me nota utilizo las palabras clave relevantes ...: - /

Publicado el 03/06/2017 a las 12:52
fuente por usuario
En otros idiomas...                            


3 respuestas

votos
9

Para responder a la pregunta general sobre qué son necesarias las funciones de la biblioteca para dar resultados correctamente redondeados:

De punto flotante es difícil, y muchas veces contrario a la intuición. No todos los programadores ha leído lo que deberían tener . Cuando las bibliotecas utilizan para permitir algún redondeo ligeramente inexacta, personas se quejaron de la precisión de la función de la biblioteca cuando sus cálculos inexactos, inevitablemente, se fueron sin sentido equivocado y producido. En respuesta, los escritores hicieron sus bibliotecas biblioteca redondeados exactamente, por lo que ahora la gente no puede echar la culpa a ellos.

En muchos casos, el conocimiento específico acerca de los algoritmos de coma flotante puede producir mejoras considerables a la exactitud y / o el rendimiento, al igual que en el caso de prueba:

Tomando el exp()de números muy cerca 0de los números de punto flotante es problemática, ya que el resultado es un número cercano a 1, mientras que toda la precisión está en la diferencia a uno, por lo que los dígitos más significativos se pierden. Es más preciso (y significativamente más rápido en este caso de prueba) para calcular exp(x) - 1a través de la función de biblioteca C matemáticas expm1(x). Si el exp()mismo está realmente necesitaba, todavía es mucho más rápido que hacer expm1(x) + 1.

Una preocupación similar existe para la informática log(1 + x), para la cual no es la función log1p(x).

Una solución rápida que acelera el caso de prueba, siempre que:

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

int main (void)
{
  int i;
  double a, c;
  c = 0;
  clock_t start = clock ();
  for (i = 0; i < 1e6; ++i) // Doing a large number of times the same type of computation with different values, to smoothen random fluctuations.
    {
      a = (double) (1 + 2 * (rand () % 0x400)) / 0x20000000000000; // "a" has only a few significant digits, and its last non-zero digit is at (fixed-point) position 53.
      c += expm1 (a) + 1; // replace exp() with expm1() + 1
    }
  clock_t stop = clock ();
  printf ("%e\n", c); // Just to be sure that the compiler will actually perform the computation.
  printf ("Clock time spent: %d\n", stop - start);
  return 0;
}

Para este caso, los tiempos en mi máquina de este modo son:

código original

1.000000e + 06

La hora del reloj pasó: 21543338

código modificado

1.000000e + 06

La hora del reloj pasó: 55076

Los programadores con conocimientos avanzados sobre las acompañan compensaciones a veces pueden considerar el uso de resultados aproximados donde la precisión no es crítica

Para un programador experimentado puede ser posible escribir una aplicación aproximado de una función lenta utilizando métodos como el de Newton-Raphson, Taylor o polinomios de Maclaurin, específicamente redondeado inexacta funciones especiales de las bibliotecas como MKL de Intel, AMCL de AMD, la relajación de la conformidad con el estándar de punto flotante del compilador, lo que reduce la precisión de IEEE754 binary32 ( float), o una combinación de éstos.

Tenga en cuenta que una mejor descripción del problema permitiría una mejor respuesta.

Respondida el 03/06/2017 a las 15:01
fuente por usuario

votos
1

En cuanto a su comentario a la respuesta @EOF 's, el 'escribir su propio' observación de @NominalAnimal parece bastante simple aquí, incluso trivial, de la siguiente manera.

Su código original anterior parece tener un argumento posible máximo para exp () de un (+ 2 * 0x400 1) / ... 0x2000 = 4.55e-13 = (que realmente debería ser 2 * 0x3ff , y cuento con 13 ceros después de su 0x2000 ... que hace que sea 2x16 ^ 13 ). De manera que 4.55e-13 argumento máximo es muy, muy pequeño.

Y entonces la expansión de Taylor trivial es exp (a) = 1 + a + (a ^ 2) / 2 + (a ^ 3) / 6 + ... que ya se le da toda la precisión del doble para esas pequeñas argumentos. Ahora, tendrá que descartar la 1 parte, como se explicó anteriormente, y después de que simplemente se reduce a expm1 (a) = a * (1 + a * (1 + a / 3.) / 2.) Y eso debe ir bastante maldito rápido! Sólo asegúrese de que una se queda pequeña. Si se pone un poco más grande, sólo tiene que añadir el siguiente término, a ^ 4/24 (que ver cómo hacer eso?).

>> << EDITAR

He modificado el programa de pruebas de la OP de la siguiente manera para probar un poco más las cosas (discusión siguiente código)

/* https://stackoverflow.com/questions/44346371/
   i-do-not-want-correct-rounding-for-function-exp/44397261 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define BASE 16               /*denominator will be (multiplier)xBASE^EXPON*/
#define EXPON 13
#define taylorm1(a) (a*(1.+a*(1.+a/3.)/2.)) /*expm1() approx for small args*/

int main (int argc, char *argv[]) {
  int N          = (argc>1?atoi(argv[1]):1e6),
      multiplier = (argc>2?atoi(argv[2]):2),
      isexp      = (argc>3?atoi(argv[3]):1); /* flags to turn on/off exp() */
  int isexpm1    = 1;                        /* and expm1() for timing tests*/
  int i, n=0;
  double denom = ((double)multiplier)*pow((double)BASE,(double)EXPON);
  double a, c=0.0, cm1=0.0, tm1=0.0;
  clock_t start = clock();
  n=0;  c=cm1=tm1=0.0;
  /* --- to smooth random fluctuations, do the same type of computation
         a large number of (N) times with different values --- */
  for (i=0; i<N; i++) {
    n++;
    a = (double)(1 + 2*(rand()%0x400)) / denom; /* "a" has only a few
                                 significant digits, and its last non-zero
                                 digit is at (fixed-point) position 53. */
    if ( isexp ) c += exp(a); /* turn this off to time expm1() alone */
    if ( isexpm1 ) {          /* you can turn this off to time exp() alone, */
      cm1 += expm1(a);        /* but difference is negligible */
      tm1 += taylorm1(a); }
    } /* --- end-of-for(i) --- */
  int nticks = (int)(clock()-start);
  printf ("N=%d, denom=%dx%d^%d, Clock time: %d (%.2f secs)\n",
         n, multiplier,BASE,EXPON,
         nticks, ((double)nticks)/((double)CLOCKS_PER_SEC));
  printf ("\t c=%.20e,\n\t c-n=%e, cm1=%e, tm1=%e\n",
           c,c-(double)n,cm1,tm1);
  return 0;
  } /* --- end-of-function main() --- */

Compilar y ejecutarlo como prueba de reproducir de OP 0x2000 ... escenario, o bien puede hacerlo con (hasta tres) argumentos opcionales timeexp númeroPruebas prueba multiplicador donde númeroPruebas por defecto en el OP de 1.000.000 , y multipler por defecto a 2 para el PO de 2x16 ^ 13 (cambiarlo a 4 , etc, por sus otras pruebas). Para el último arg, timeexp , introduzca un 0 a hacer sólo el expm1 () (y mi innecesaria Taylor-like) cálculo. El punto de esto es mostrar que los malos-timing-casos mostrados por el PO desaparecen con expm1 () , que tiene "muy poco tiempo", independientemente de multiplicador .

Así se ejecuta por defecto, prueba y prueba 1000000 4 , producir (bueno, me llamaron del programa de redondeo ) ...

bash-4.3$ ./rounding 
N=1000000, denom=2x16^13, Clock time: 11155070 (11.16 secs)
         c=1.00000000000000023283e+06,
         c-n=2.328306e-10, cm1=1.136017e-07, tm1=1.136017e-07
bash-4.3$ ./rounding 1000000 4
N=1000000, denom=4x16^13, Clock time: 200211 (0.20 secs)
         c=1.00000000000000011642e+06,
         c-n=1.164153e-10, cm1=5.680083e-08, tm1=5.680083e-08

Así que lo primero que se le nota que es de la OP cn utilizando exp () difiere sustancialmente de ambos TM1 CM1 == usando expm1 () y mi taylor aprox. Si se reduce N entran en acuerdo, de la siguiente manera ...

N=10, denom=2x16^13, Clock time: 941 (0.00 secs)
         c=1.00000000000007140954e+01,
         c-n=7.140954e-13, cm1=7.127632e-13, tm1=7.127632e-13
bash-4.3$ ./rounding 100
N=100, denom=2x16^13, Clock time: 5506 (0.01 secs)
         c=1.00000000000010103918e+02,
         c-n=1.010392e-11, cm1=1.008393e-11, tm1=1.008393e-11
bash-4.3$ ./rounding 1000
N=1000, denom=2x16^13, Clock time: 44196 (0.04 secs)
         c=1.00000000000011345946e+03,
         c-n=1.134595e-10, cm1=1.140730e-10, tm1=1.140730e-10
bash-4.3$ ./rounding 10000
N=10000, denom=2x16^13, Clock time: 227215 (0.23 secs)
         c=1.00000000000002328306e+04,
         c-n=2.328306e-10, cm1=1.131288e-09, tm1=1.131288e-09
bash-4.3$ ./rounding 100000
N=100000, denom=2x16^13, Clock time: 1206348 (1.21 secs)
         c=1.00000000000000232831e+05,
         c-n=2.328306e-10, cm1=1.133611e-08, tm1=1.133611e-08

Y por lo que el tiempo de exp () frente expm1 () se refiere, ver por sí mismo ...

bash-4.3$ ./rounding 1000000 2  
N=1000000, denom=2x16^13, Clock time: 11168388 (11.17 secs)
         c=1.00000000000000023283e+06,
         c-n=2.328306e-10, cm1=1.136017e-07, tm1=1.136017e-07
bash-4.3$ ./rounding 1000000 2 0
N=1000000, denom=2x16^13, Clock time: 24064 (0.02 secs)
         c=0.00000000000000000000e+00,
         c-n=-1.000000e+06, cm1=1.136017e-07, tm1=1.136017e-07

Pregunta: Usted notará que una vez que el exp () cálculo llega a N = 10000 ensayos, su suma permanece constante, independientemente de mayor N . No sé por qué que estaría sucediendo.

>> __ __ SEGUNDA EDICIÓN <<

De acuerdo, @EOF, "que me hizo ver" con su comentario "acumulación jerárquica". Y que de hecho trabaja para llevar la exp () suma más cerca (más cerca) a la (presumiblemente correcta) expm1 () suma. Del código modificado inmediatamente por debajo seguido por una discusión. Pero una discusión nota aquí: recuerdo multiplicador desde arriba. Eso se ha ido, y en su mismo lugar se Exportan de manera que el denominador es ahora 2 ^ expon donde el valor predeterminado es 53 , a juego por defecto de OP (y creo que una mejor adecuación cómo ella estaba pensando en ello). Está bien, y aquí está el código ...

/* https://stackoverflow.com/questions/44346371/
   i-do-not-want-correct-rounding-for-function-exp/44397261 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define BASE 2                /*denominator=2^EXPON, 2^53=2x16^13 default */
#define EXPON 53
#define taylorm1(a) (a*(1.+a*(1.+a/3.)/2.)) /*expm1() approx for small args*/

int main (int argc, char *argv[]) {
  int N          = (argc>1?atoi(argv[1]):1e6),
      expon      = (argc>2?atoi(argv[2]):EXPON),
      isexp      = (argc>3?atoi(argv[3]):1), /* flags to turn on/off exp() */
      ncparts    = (argc>4?atoi(argv[4]):1), /* #partial sums for c */
      binsize    = (argc>5?atoi(argv[5]):10);/* #doubles to sum in each bin */
  int isexpm1    = 1;                        /* and expm1() for timing tests*/
  int i, n=0;
  double denom = pow((double)BASE,(double)expon);
  double a, c=0.0, cm1=0.0, tm1=0.0;
  double csums[10], cbins[10][65537]; /* c partial sums and heirarchy */
  int nbins[10], ibin=0;      /* start at lowest level */
  clock_t start = clock();
  n=0;  c=cm1=tm1=0.0;
  if ( ncparts > 65536 ) ncparts=65536;  /* array size check */
  if ( ncparts > 1 ) for(i=0;i<ncparts;i++) cbins[0][i]=0.0; /*init bin#0*/
  /* --- to smooth random fluctuations, do the same type of computation
         a large number of (N) times with different values --- */
  for (i=0; i<N; i++) {
    n++;
    a = (double)(1 + 2*(rand()%0x400)) / denom; /* "a" has only a few
                                 significant digits, and its last non-zero
                                 digit is at (fixed-point) position 53. */
    if ( isexp ) {            /* turn this off to time expm1() alone */
      double expa = exp(a);   /* exp(a) */
      c += expa;              /* just accumulate in a single "bin" */
      if ( ncparts > 1 ) cbins[0][n%ncparts] += expa; } /* accum in ncparts */
    if ( isexpm1 ) {          /* you can turn this off to time exp() alone, */
      cm1 += expm1(a);        /* but difference is negligible */
      tm1 += taylorm1(a); }
    } /* --- end-of-for(i) --- */
  int nticks = (int)(clock()-start);
  if ( ncparts > 1 ) {        /* need to sum the partial-sum bins */
    nbins[ibin=0] = ncparts;  /* lowest-level has everything */
    while ( nbins[ibin] > binsize ) { /* need another heirarchy level */
      if ( ibin >= 9 ) break; /* no more bins */
      ibin++;                 /* next available heirarchy bin level */
      nbins[ibin] = (nbins[ibin-1]+(binsize-1))/binsize; /*#bins this level*/
      for(i=0;i<nbins[ibin];i++) cbins[ibin][i]=0.0; /* init bins */
      for(i=0;i<nbins[ibin-1];i++) {
        cbins[ibin][(i+1)%nbins[ibin]] += cbins[ibin-1][i]; /*accum in nbins*/
        csums[ibin-1] += cbins[ibin-1][i]; } /* accumulate in "one bin" */
      } /* --- end-of-while(nprevbins>binsize) --- */
    for(i=0;i<nbins[ibin];i++) csums[ibin] += cbins[ibin][i]; /*highest level*/
    } /* --- end-of-if(ncparts>1) --- */
  printf ("N=%d, denom=%d^%d, Clock time: %d (%.2f secs)\n", n, BASE,expon,
         nticks, ((double)nticks)/((double)CLOCKS_PER_SEC));
  printf ("\t c=%.20e,\n\t c-n=%e, cm1=%e, tm1=%e\n",
           c,c-(double)n,cm1,tm1);
  if ( ncparts > 1 ) { printf("\t binsize=%d...\n",binsize);
    for (i=0;i<=ibin;i++) /* display heirarchy */
      printf("\t level#%d: #bins=%5d, c-n=%e\n",
      i,nbins[i],csums[i]-(double)n); }
  return 0;
  } /* --- end-of-function main() --- */

Bien, y ahora se puede observar que las dos argumentos adicionales de línea de comandos siguientes a la edad timeexp . Son ncparts para el número inicial de contenedores en el que todo el númeroPruebas serán distribuidos. Por lo tanto en el nivel más bajo de la jerarquía, cada contenedor debe (errores de módulo :) tienen la suma de los ensayos # / ncparts dobles. El argumento después de que es binsize , que será el número de dobles sumadas en cada bin en cada nivel sucesivo, hasta el último nivel tiene menos (o iguales) #bins como binsize . Así que aquí está un ejemplo dividiendo 1000000 ensayos en 50000 contenedores, lo que significa 20doubles / bin en el nivel más bajo, y 5doubles / bin a partir de entonces ...

bash-4.3$ ./rounding 1000000 53 1 50000 5 
N=1000000, denom=2^53, Clock time: 11129803 (11.13 secs)
         c=1.00000000000000465661e+06,
         c-n=4.656613e-09, cm1=1.136017e-07, tm1=1.136017e-07
         binsize=5...
         level#0: #bins=50000, c-n=4.656613e-09
         level#1: #bins=10002, c-n=1.734588e-08
         level#2: #bins= 2002, c-n=7.974450e-08
         level#3: #bins=  402, c-n=1.059379e-07
         level#4: #bins=   82, c-n=1.133885e-07
         level#5: #bins=   18, c-n=1.136214e-07
         level#6: #bins=    5, c-n=1.138542e-07

Nótese cómo el CN para exp () converge bastante bien hacia el expm1 () valor. Pero tenga en cuenta la forma en que lo mejor es a nivel # 5, y no converge uniformemente en absoluto. Y tenga en cuenta si se rompe las númeroPruebas en sólo 5.000 contenedores iniciales, se obtiene tan buenos resultado,

bash-4.3$ ./rounding 1000000 53 1 5000 5
N=1000000, denom=2^53, Clock time: 11165924 (11.17 secs)
         c=1.00000000000003527384e+06,
         c-n=3.527384e-08, cm1=1.136017e-07, tm1=1.136017e-07
         binsize=5...
         level#0: #bins= 5000, c-n=3.527384e-08
         level#1: #bins= 1002, c-n=1.164153e-07
         level#2: #bins=  202, c-n=1.158332e-07
         level#3: #bins=   42, c-n=1.136214e-07
         level#4: #bins=   10, c-n=1.137378e-07
         level#5: #bins=    4, c-n=1.136214e-07

De hecho, jugando con ncparts y binsize no parece mostrar mucha sensibilidad, y no siempre es "más es mejor" (es decir, menos de binsize ) tampoco. Así que no estoy seguro exactamente lo que está pasando. Podría ser un error (o dos), o podría ser otra pregunta para @EOF ... ???

>> EDITAR - ejemplo que muestra además par jerarquía "árbol binario" <<

Ejemplo a continuación añadido como por comentario @EOF 's (Nota:. Re-copia anterior código que tenía que editar nbins [ibin] cálculo para cada nivel junto a nbins [ibin] = (nbins [ibin-1] + (binsize-1 )) / binsize; de nbins [ibin] = (nbins [ibin-1] + 2 * binsize) / binsize; que era "demasiado conservadora" para crear ... 16,8,4,2 secuencia)

bash-4.3$ ./rounding 1024 53 1 512 2
N=1024, denom=2^53, Clock time: 36750 (0.04 secs)
         c=1.02400000000011573320e+03,
         c-n=1.157332e-10, cm1=1.164226e-10, tm1=1.164226e-10
         binsize=2...
         level#0: #bins=  512, c-n=1.159606e-10
         level#1: #bins=  256, c-n=1.166427e-10
         level#2: #bins=  128, c-n=1.166427e-10
         level#3: #bins=   64, c-n=1.161879e-10
         level#4: #bins=   32, c-n=1.166427e-10
         level#5: #bins=   16, c-n=1.166427e-10
         level#6: #bins=    8, c-n=1.166427e-10
         level#7: #bins=    4, c-n=1.166427e-10
         level#8: #bins=    2, c-n=1.164153e-10

>> EDITAR - para mostrar @ solución elegante de EOF en el comentario a continuación <<

"Además Par" se puede lograr con elegancia de forma recursiva, según comentario de @ EOF a continuación, que estoy reproduciendo aquí. (Nota caso 0/1 a fin de recursión para manejar n par / impar.)

  /* Quoting from EOF's comment...
   What I (EOF) proposed is effectively a binary tree of additions:
   a+b+c+d+e+f+g+h as ((a+b)+(c+d))+((e+f)+(g+h)).
   Like this: Add adjacent pairs of elements, this produces
   a new sequence of n/2 elements.
   Recurse until only one element is left.
   (Note that this will require n/2 elements of storage,
   rather than a fixed number of bins like your implementation) */
  double trecu(double *vals, double sum, int n) {
      int midn = n/2;
      switch (n) {
        case  0: break;
        case  1: sum += *vals; break;
        default: sum = trecu(vals+midn, trecu(vals,sum,midn), n-midn); break; }
      return(sum);
      } 
Respondida el 06/06/2017 a las 14:26
fuente por usuario

votos
1

Se trata de una "respuesta" / seguimiento a los comentarios anteriores de EOF re su Trecu (algoritmo y código) por su sugerencia "suma árbol binario". "Requisitos previos" antes de leer este están leyendo esta discusión. Sería bueno para recoger todo lo que en un lugar organizado, pero no he hecho todavía ...

... Lo que hice fue construir Trecu de EOF () en el programa de pruebas de la respuesta anterior que yo había escrito mediante la modificación del programa de prueba original de la OP. Pero entonces me di cuenta de que Trecu () genera exactamente (y me refiero exactamente ) la misma respuesta que la "suma simple" c usando exp () , no la suma CM1 usando expm1 () que nos lo esperábamos de un árbol binario más precisa suma.

Pero ese programa de prueba es un poco (tal vez dos bits :) "complicadas" (o, como dijo EOF, "leer"), por lo que escribió un programa de prueba más pequeña, se indican a continuación (con ejemplo ejecuta y discusión por debajo), que por separado prueba / ejercicio Trecu (). Por otra parte, también escribí función bintreesum () en el código de abajo, que abstrae / encapsula el código iterativo para la suma árbol binario que había incrustado en el programa de prueba anterior. En ese caso anterior, mi código iterativo de hecho estuvo cerca de la CM1 respuesta, por lo que me había esperado Trecu recursiva de EOF () para hacer lo mismo. A largo y corto de él es que, a continuación, pasa lo mismo - bintreesum () se mantiene cerca de corregir la respuesta, mientras que Trecu () se aleja, que reproduce exactamente la "suma simple".

Lo que estamos sumando a continuación es simplemente la suma (i), i = 1 ... n, que es sólo el n conocido (n + 1) / 2. Pero eso no es del todo bien - para reproducir el problema de OP, sumando no es la suma (i) por sí sola, sino que suma (1 + i * 10 ^ (- e)), donde e se puede dar en la línea de comandos. Así que para, por ejemplo, n = 5, no obtiene 15 sino 5.000 ... 00015, o para n = 6 se obtienen 6.000 ... 00021, etc, y para evitar una larga formato, mucho tiempo, yo printf ( ) suma-n para eliminar que parte entera. ¿¿¿Bueno??? Así que aquí está el código ...

/* Quoting from EOF's comment...
   What I (EOF) proposed is effectively a binary tree of additions:
   a+b+c+d+e+f+g+h as ((a+b)+(c+d))+((e+f)+(g+h)).
   Like this: Add adjacent pairs of elements, this produces
   a new sequence of n/2 elements.
   Recurse until only one element is left. */
#include <stdio.h>
#include <stdlib.h>

double trecu(double *vals, double sum, int n) {
  int midn = n/2;
  switch (n) {
    case  0: break;
    case  1: sum += *vals; break;
    default: sum = trecu(vals+midn, trecu(vals,sum,midn), n-midn); break; }
  return(sum);
  } /* --- end-of-function trecu() --- */

double bintreesum(double *vals, int n, int binsize) {
  double binsum = 0.0;
  int nbin0 = (n+(binsize-1))/binsize,
      nbin1 = (nbin0+(binsize-1))/binsize,
      nbins[2] = { nbin0, nbin1 };
  double *vbins[2] = {
            (double *)malloc(nbin0*sizeof(double)),
            (double *)malloc(nbin1*sizeof(double)) },
         *vbin0=vbins[0], *vbin1=vbins[1];
  int ibin=0, i;
  for ( i=0; i<nbin0; i++ ) vbin0[i] = 0.0;
  for ( i=0; i<n; i++ ) vbin0[i%nbin0] += vals[i];
  while ( nbins[ibin] > 1 ) {
    int jbin = 1-ibin;        /* other bin, 0<-->1 */
    nbins[jbin] = (nbins[ibin]+(binsize-1))/binsize;
    for ( i=0; i<nbins[jbin]; i++ ) vbins[jbin][i] = 0.0;
    for ( i=0; i<nbins[ibin]; i++ )
      vbins[jbin][i%nbins[jbin]] += vbins[ibin][i];
    ibin = jbin;              /* swap bins for next pass */
    } /* --- end-of-while(nbins[ibin]>0) --- */
  binsum = vbins[ibin][0];
  free((void *)vbins[0]);  free((void *)vbins[1]);
  return ( binsum );
  } /* --- end-of-function bintreesum() --- */

#if defined(TESTTRECU)
#include <math.h>
#define MAXN (2000000)
int main(int argc, char *argv[]) {
  int N       = (argc>1? atoi(argv[1]) : 1000000 ),
      e       = (argc>2? atoi(argv[2]) : -10 ),
      binsize = (argc>3? atoi(argv[3]) : 2 );
  double tens = pow(10.0,(double)e);
  double *vals = (double *)malloc(sizeof(double)*MAXN),
         sum = 0.0;
  double trecu(), bintreesum();
  int i;
  if ( N > MAXN ) N=MAXN;
  for ( i=0; i<N; i++ ) vals[i] = 1.0 + tens*(double)(i+1);
  for ( i=0; i<N; i++ ) sum += vals[i];
  printf(" N=%d, Sum_i=1^N {1.0 + i*%.1e} - N  =  %.8e,\n"
         "\t plain_sum-N  = %.8e,\n"
         "\t trecu-N      = %.8e,\n"
         "\t bintreesum-N = %.8e \n",
         N, tens, tens*((double)N)*((double)(N+1))/2.0,
          sum-(double)N,
         trecu(vals,0.0,N)-(double)N,
         bintreesum(vals,N,binsize)-(double)N );
  } /* --- end-of-function main() --- */
#endif

Así que si guarda como trecu.c, a continuación, compilar como cc -DTESTTRECU trecu.c -lm -o Trecu y vuelva a ejecutar con cero a tres argumentos de línea de comandos opcionales como Trecu númeroPruebas e binsize predeterminados son # 1000000 (= ensayos como el programa de OP), e = -10, y = 2 función (por mi bintreesum () para hacer una suma árbol binario en lugar de contenedores de mayor tamaño binsize).

Y aquí hay algunos resultados de las pruebas que ilustran el problema descrito anteriormente,

bash-4.3$ ./trecu              
 N=1000000, Sum_i=1^N {1.0 + i*1.0e-10} - N  =  5.00000500e+01,
         plain_sum-N  = 5.00000500e+01,
         trecu-N      = 5.00000500e+01,
         bintreesum-N = 5.00000500e+01 
bash-4.3$ ./trecu 1000000 -15
 N=1000000, Sum_i=1^N {1.0 + i*1.0e-15} - N  =  5.00000500e-04,
         plain_sum-N  = 5.01087168e-04,
         trecu-N      = 5.01087168e-04,
         bintreesum-N = 5.00000548e-04 
bash-4.3$ 
bash-4.3$ ./trecu 1000000 -16
 N=1000000, Sum_i=1^N {1.0 + i*1.0e-16} - N  =  5.00000500e-05,
         plain_sum-N  = 6.67552231e-05,
         trecu-N      = 6.67552231e-05,
         bintreesum-N = 5.00001479e-05 
bash-4.3$ 
bash-4.3$ ./trecu 1000000 -17
 N=1000000, Sum_i=1^N {1.0 + i*1.0e-17} - N  =  5.00000500e-06,
         plain_sum-N  = 0.00000000e+00,
         trecu-N      = 0.00000000e+00,
         bintreesum-N = 4.99992166e-06 

Así se puede ver que para el todo de ejecución predeterminado, e = -10, todos lo están haciendo bien. Es decir, la línea superior que dice "Sum" sólo lo hace el n (n + 1) / 2 cosa, por lo que presumiblemente indica la respuesta correcta. Y todo el mundo está de acuerdo en que a continuación para el caso por defecto e = -10 prueba. Sin embargo, para el e = -15 y e = -16 casos por debajo de eso, Trecu () coincide exactamente con la plain_sum, mientras bintreesum mantiene bastante cerca de la respuesta correcta. Y, por último, para e = -17, plain_sum y Trecu () han "desaparecido", mientras que bintreesum () 's sigue aguantando bastante bien.

Así Trecu () 's haciendo correctamente la suma de acuerdo, pero de su recursividad aparentemente no hacer ese tipo 'árbol binario' de cosas que mi bintreesum iterativo más sencillo ()' s aparentemente haciendo correctamente. Y que de hecho demuestra que la sugerencia del EOF para "suma árbol binario" se da cuenta de una gran mejora sobre el plain_sum para éstos + 1 épsilon tipo de casos. Por lo que nos gusta mucho ver a su Trecu) trabajo recursividad (!!! Cuando originalmente miraba, pensé que lo hizo el trabajo. Pero ese doble recursividad (¿hay un nombre especial para eso?) En su defecto: caso es aparentemente más confuso (al menos para mí :) de lo que pensaba. Como he dicho, que está haciendo la suma, pero no lo "árbol binario".

Bueno, por lo que le gustaría asumir el reto y explicar lo que está pasando en ese Trecu recursividad ()? Y, tal vez lo más importante, solucionarlo por lo que lo que está destinado. Gracias.

Respondida el 08/06/2017 a las 02:34
fuente por usuario

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more