Estoy tratando de escribir un programa que calcule dígitos decimales de π a 1000 dígitos o más.
Para practicar la programación de bajo nivel por diversión, el programa final se escribirá en ensamblador, en una CPU de 8 bits que no tiene multiplicaciones ni divisiones, y solo realiza sumas de 16 bits. Para facilitar la implementación, es deseable poder usar solo operaciones con enteros sin signo de 16 bits y usar un algoritmo iterativo. La velocidad no es una preocupación importante. Y la multiplicación y división rápidas está más allá del alcance de esta pregunta, así que no consideres esos problemas también.
Antes de implementarlo en ensamblaje, todavía estoy tratando de encontrar un algoritmo utilizable en C en mi computadora de escritorio. Hasta ahora, encontré que la siguiente serie es razonablemente eficiente y relativamente fácil de implementar.
La fórmula se deriva de la serie de Leibniz utilizando una técnica de aceleración de convergencia. Para obtenerla, consulte Computing the Digits in π, de Carl D. Offner (https://cs.umb.edu/~offner/files/pi.pdf), página 19-26. La fórmula final se muestra en la página 26. La fórmula inicial que escribí tenía algunos errores tipográficos, actualice la página para ver la fórmula corregida. El término constante 2
como máximo, se explica en la página 54. El documento también describía un algoritmo iterativo avanzado, pero no lo usé aquí.
Si uno evalúa la serie usando muchos (por ejemplo, 5000) términos, es posible obtener miles de dígitos de π fácilmente, y encontré que esta serie también es fácil de evaluar iterativamente usando este algoritmo:
Algoritmo
- Primero, reorganiza la fórmula para obtener sus términos constantes de una matriz.
-
Complete la matriz con 2 para comenzar la primera iteración, por lo tanto, la nueva fórmula se parece a la original.
-
Dejar
carry = 0
. -
Empiece por el término mayor. Obtenga un término (2) de la matriz, multiplique el término por
PRECISION
para realizar una división de punto fijo contra2 * i + 1
y guarde el recordatorio como el nuevo término de la matriz. Luego suma el siguiente término. Ahora decrementari
pasar al siguiente término, repetir hastai == 1
. Finalmente agregue el término finalx_0
. -
Debido a que se usa un entero de 16 bits,
PRECISION
es10
, por lo tanto se obtienen 2 dígitos decimales, pero solo el primer dígito es válido. Guarde el segundo dígito como acarreo. Mostrar el primer dígito más llevar. -
x_0
es el entero 2, no se debe sumar para las iteraciones sucesivas, borrarlo. -
Ir al paso 4 para calcular el siguiente dígito decimal, hasta que tengamos todos los dígitos que queremos.
Implementación 1
Traduciendo este algoritmo a C:
#include <stdio.h>
#include <stdint.h>
#define N 2160
#define PRECISION 10
uint16_t terms[N + 1] = {0};
int main(void)
{
/* initialize the initial terms */
for (size_t i = 0; i < N + 1; i++) {
terms[i] = 2;
}
uint16_t carry = 0;
for (size_t j = 0; j < N / 4; j++) {
uint16_t numerator = 0;
uint16_t denominator;
uint16_t digit;
for (size_t i = N; i > 0; i--) {
numerator += terms[i] * PRECISION;
denominator = 2 * i + 1;
terms[i] = numerator % denominator;
numerator /= denominator;
numerator *= i;
}
numerator += terms[0] * PRECISION;
digit = numerator / PRECISION + carry;
carry = numerator % PRECISION;
printf("%01u", digit);
/* constant term 2, only needed for the first iteration. */
terms[0] = 0;
}
putchar('\n');
}
El código puede calcular π con 31 dígitos decimales, hasta que comete un error.
31415926535897932384626433832794
10 <-- wrong
Algunas veces digit + carry
es mayor que 9, por lo que necesita un acarreo adicional. Si tenemos mucha mala suerte, puede haber incluso un doble acarreo, un triple acarreo, etc. Usamos un ring-buffer para almacenar los últimos 4 dígitos. Si se detecta un acarreo adicional, generamos un retroceso para borrar el dígito anterior, realizamos un acarreo y los reimprimimos. Esta es solo una solución fea para la Prueba de concepto, que es irrelevante para mi pregunta sobre el desbordamiento, pero para completar, aquí está. Algo mejor sería implementado en el futuro.
Implementación 2 con acarreo repetido
#include <stdio.h>
#include <stdint.h>
#define N 2160
#define PRECISION 10
#define BUF_SIZE 4
uint16_t terms[N + 1] = {0};
int main(void)
{
/* initialize the initial terms */
for (size_t i = 0; i < N + 1; i++) {
terms[i] = 2;
}
uint16_t carry = 0;
uint16_t digit[BUF_SIZE];
int8_t idx = 0;
for (size_t j = 0; j < N / 4; j++) {
uint16_t numerator = 0;
uint16_t denominator;
for (size_t i = N; i > 0; i--) {
numerator += terms[i] * PRECISION;
denominator = 2 * i + 1;
terms[i] = numerator % denominator;
numerator /= denominator;
numerator *= i;
}
numerator += terms[0] * PRECISION;
digit[idx] = numerator / PRECISION + carry;
/* over 9, needs at least one carry op. */
if (digit[idx] > 9) {
for (int i = 1; i <= 4; i++) {
if (i > 3) {
/* allow up to 3 consecutive carry ops */
fprintf(stderr, "ERROR: too many carry ops!\n");
return 1;
}
/* erase a digit */
putchar('\b');
/* carry */
digit[idx] -= 10;
idx--;
if (idx < 0) {
idx = BUF_SIZE - 1;
}
digit[idx]++;
if (digit[idx] < 10) {
/* done! reprint the digits */
for (int j = 0; j <= i; j++) {
printf("%01u", digit[idx]);
idx++;
if (idx > BUF_SIZE - 1) {
idx = 0;
}
}
break;
}
}
}
else {
printf("%01u", digit[idx]);
}
carry = numerator % PRECISION;
terms[0] = 0;
/* put an element to the ring buffer */
idx++;
if (idx > BUF_SIZE - 1) {
idx = 0;
}
}
putchar('\n');
}
Genial, ahora el programa puede calcular correctamente 534 dígitos de π, hasta que comete un error.
3141592653589793238462643383279502884
1971693993751058209749445923078164062
8620899862803482534211706798214808651
3282306647093844609550582231725359408
1284811174502841027019385211055596446
2294895493038196442881097566593344612
8475648233786783165271201909145648566
9234603486104543266482133936072602491
4127372458700660631558817488152092096
2829254091715364367892590360011330530
5488204665213841469519415116094330572
7036575959195309218611738193261179310
5118548074462379962749567351885752724
8912279381830119491298336733624406566
43086021394946395
22421 <-- wrong
Desbordamiento de enteros de 16 bits
Resulta que, durante el cálculo de los términos más grandes al principio, el término de error se vuelve bastante grande, ya que los divisores al principio están en el rango de ~4000. Al evaluar la serie, numerator
en realidad comienza a desbordarse en la multiplicación inmediatamente.
El desbordamiento de enteros es insignificante a la hora de calcular los primeros 500 dígitos, pero va empeorando cada vez más, hasta dar un resultado incorrecto.
Cambiando uint16_t numerator = 0
para uint32_t numerator = 0
puede resolver este problema y calcular π a más de 1000 dígitos.
Sin embargo, como mencioné antes, mi plataforma de destino es una CPU de 8 bits y solo tiene operaciones de 16 bits. ¿Hay algún truco para resolver el problema de desbordamiento de enteros de 16 bits que estoy viendo aquí? usando solo uno o más uint16_t? Si no es posible evitar la aritmética de precisión múltiple, ¿cuál es el método más simple para implementarla aquí? Sé que de alguna manera necesito introducir una “palabra de extensión” adicional de 16 bits, pero no estoy seguro de cómo puedo implementarla.
Y gracias de antemano por su paciencia para comprender el largo contexto aquí.
Puede ver lo que hizo Abseil para implementar uint128 con uint64s, ya que son las mismas ideas básicas: github.com/abseil/abseil-cpp/blob/master/absl/numeric/int128.h y github.com/abseil/abseil-cpp/blob/master/absl/numeric/int128.cc
–David Eisenstat
7 mayo 2019 a las 12:47
@比尔盖子 ¿De dónde sacaste esto?
Pi
¿fórmula de cálculo?– yW0K5o
7 mayo 2019 a las 12:58
@yW0K5o La fórmula se deriva de la serie de Leibniz utilizando una técnica de aceleración de convergencia. Para obtenerla, consulte Cálculo de los dígitos en πpor Carl D. Offner (cs.umb.edu/~offner/files/pi.pdf), página 19-26. La fórmula final se muestra en la página 26. El término constante
2
como máximo, se explica en la página 54. Mi fórmula inicial que escribí tenía algunos errores tipográficos, actualice la página para ver la fórmula fija. El documento también describía un algoritmo iterativo avanzado, pero no lo usé aquí.– 比尔盖子
7 de mayo de 2019 a las 13:03
¿Quiere detectar el desbordamiento o calcular a pesar de los desbordamientos? (en el último caso, no creo que pueda evitar la aritmética de precisión múltiple).
– Yves Daoust
7 mayo 2019 a las 13:34
Probablemente necesite precisión “doble”.
– Yves Daoust
7 mayo 2019 a las 13:43