terça-feira, junho 15, 2021

Calculando Pi na Plaquinha (Uno, PiPico, ESP32, BlackPill, etc)

Uma coisa que está na minha lista faz algum tempo é fazer testes de velocidade com as várias plaquinhas que eu tenho. Mais pela brincadeira, pois estes testes normalmente não representam direito o desempenho em aplicações reais. E aí me surgiu a ideia de fazer a comparação calculando os dígitos do Pi.


Chovendo no molhado, o Pi é a razão entre a circunferência de um círculo e o seu diâmetro. É um número "irracional", que não pode ser colocado na forma de fração e na forma decimal tem um número infinito de dígitos. Calcular os dígitos do Pi tem ocupado pessoas desde muito tempo atrás (a wikipedia tem uma cronologia).

A partir de 1400 sabe-se que o Pi pode ser calculado através da soma de séries infinitas. Com o advento do computador foram desenvolvidos algorítimos que facilitam e aceleram o cálculo. Na minha busca eu rapidamente achei o seguinte programa (de autoria de Dik T Winter):

int a=10000,b,c=2800,d,e,f[2801],g;main(){for(;b-c;)f[b++]=a/5;
for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)for(b=c;d+=f[b]*a,
f[b]=d%--g,d/=g--,--b;d*=b);}

Você talvez encontre uma versão com pequenas diferenças, esta acima calcula os primeiros 800 dígitos do Pi. Este programa está, propositalmente, compactado e ofuscado. Para os meus testes eu fiz uma versão um pouco mais legível e adaptada para compilação na IDE do Arduino:

/*
 * Calculo dos dígito de Pi
 * Usando o algoritimo Spigot de Rabinowitz e Wagon
 * Adaptação da versão compacta e obsfucada escrita por Dik T. Winter:
 * 
 * int a=10000,b,c=2800,d,e,f[2801],g;main(){for(;b-c;)f[b++]=a/5;
 * for(;d=0,g=c*2;c-=14,printf("%.4d",e+d/a),e=d%a)for(b=c;d+=f[b]*a,
 * f[b]=d%--g,d/=g--,--b;d*=b);}
 * 
 * Comentários nas declarações copiados de
 * https://stackoverflow.com/questions/4084571/implementing-the-spigot-algorithm-for-%CF%80-pi
 * 
 * Daniel Quadros junho/2021
 */
 
#define NDIGITS 10000          //max digits to compute
#define LEN (NDIGITS/4+1)*14   //nec. array length
 
int32_t a = 10000;             //new base, 4 decimal digits
int32_t b = 0;                 //nominator prev. base
int32_t c = LEN;               //index
int32_t d = 0;                 //accumulator and carry
int32_t e = 0;                 //save previous 4 digits
int32_t f[LEN+1];              //array of 4-digit-decimals
int32_t g = 0;                 //denom previous base

void setup() {
  delay(1000);
  Serial.begin(115200);
}

// Não usado
void loop() {
  while (Serial.read() != '\n') {
    delay(100); 
  }
  Serial.println("PI = ");
  long inicio = millis();
  calcula();
  long tempo = millis() - inicio;
  Serial.println();
  Serial.print ("Tempo = ");
  Serial.print (tempo);
  Serial.println ("ms");
}

// Cálculo dos dígitos do Pi
void calcula() {
  char dig[5] = "0000"; // para fazer o print
  int n = 0;            // para mudar de linha a cada 100 dígitos

  c = LEN;
  for(b = 0; b < c; b++) {
    f[b] = a/5;
  }

  e = 0;
  for (; c > 0; c -= 14) {
      d = 0;
      g = c*2;
      b = c;
      for (;;) {
          d += f[b]*a;
          f[b] = d % --g;
          d /= g--;
          if (--b == 0) {
              break;
          }
          d *= b;
      }
      uint16_t val = e+d/a;
      dig[0] = (val / 1000) + '0'; 
      dig[1] = ((val / 100) % 10) + '0'; 
      dig[2] = ((val / 10) % 10) + '0'; 
      dig[3] = (val % 10) + '0'; 
      Serial.print (dig);
      n += 4;
      if (n == 100) {
        Serial.println();
        n = 0;
      }
      e = d % a;
  }
}

Mas o que está sendo feito, e porque? Não me arrisco a dizer que entendi completamente, mas aqui vão algumas informações e referências. O algoritmo usado pertence a uma classe chamada "spigot" (torneira): os dígitos vão sendo gerados na ordem e não são usados diretamente no cálculo dos dígitos seguintes. Este algorítimo específico foi criado em 1995 por  Stan Wagon and Stanley Rabinowitz.

Algumas características interessantes deste algorítimo:

  • Requer somente soma, subtração, multiplicação e divisão de inteiros
  • Os dígitos são gerados de 4 em 4
  • A quantidade de dígitos a calcular precisa ser definida a priori
  • Requer um vetor com 14 inteiros para cada conjunto de 4 dígitos
Este último ponto é importante quando falamos em microcontroladores com pouca memória. Para rodar no Arduino Uno (que tem modestos 2K) eu reduzi o número de dígitos para 200 e usei inteiros sem sinal de 16 bits no vetor. Aliás, não encontrei nenhum comentário sobre o tamanho dos inteiros utilizados no algorítimo, pelas minhas experiências dá para calcular 200 dígitos usando inteiro sem sinal de 16 bits no vetor (e inteiros de 32 bits nas demais variáveis). Usando inteiros de 32 bits em todas as variáveis calculou corretamente 20000 dígitos.

Algumas referências sobre o algorítimo:

Explicação (incompleta) e a versão ofuscada: https://crypto.stanford.edu/pbc/notes/pi/code.html
Artigo que inclui uma explicação mais completa: https://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/spigot.pdf

Eu rodei o programa (acertando o número de dígitos conforme a memória disponível) em algumas placas da minha coleção: Arduino Uno, Raspberry Pi Pico, ESP32, Black Pill (STM32) e Seeduino XIAO. O resultado esta aqui, no meu canal do YouTube.


Nenhum comentário: