Como NÃO tentar criar um gerador de valores aleatórios…

Já falei sobre o LCG (Linear Congruential Generator) e outros algoritmos como o Mersene Twster e o XorShift128+. Todos eles são “pseudo” geradores de valores aleatórios, já que não é possível gerar valores aleatórios com métodos determinísticos. Aliás, gerar valores aleatórios é um troço bem difícil porque a maioria dos fenômenos físicos tendem a oferecer um padrão, dada uma quantidade razoável de amostras. Por exemplo, se você jogar a mesma moeda, num jogo de “cara ou coroa”, digamos, 1 milhão de vezes, observará que a quantidade de “caras” ou “coroas” é maior que a outra. Ou seja, você pode prever, com maior segurança (embora não exatamente) qual será o resultado da próxima jogada…

De fato, até onde sei, os únicos fenômenos aleatórios são ao nível quântico (variação de fuga de carga num semnicondutor aquecido, por exemplo… Ou o decaimento radioativo num intervalo de tempo pequeno o suficiente)… De fato, também já falei por aqui, um desses fenômenos é usado, hoje em dia, para disponibilizar um DTRNG (Digital True Random Number Generator), presente dentro de todos os processadores da família Intel (pelo menos nos Ivy Bridge em diante) e acessíveis através da instrução RDRAND.

Mas, note: Mesmo que o LCG e outros métodos não criem valores verdadeiramente aleatórios, num escopo restrito e onde não há preocupação com a “aleatoriedade absoluta”, eles podem ser encacrados como geradores aleatórios reais. Abaixo, mostro um programinha que cria uma imagem de 1024×1024 pixels (RGB) com base na função rand() da biblioteca de C:

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

// Scale 'n' to 24 bits (0xBBGGRR).
#define rand2rgb(n) \
  ( ( int ) ( ( (n) * 16777215.0 ) / RAND_MAX ) )

int main ( void )
{
  int x, y, r;

  srand( time(NULL) );

  fputs( "P6\n1024 1024\n255\n", stdout );

  for ( y = 0; y < 1024; y++ )
    for ( x = 0; x < 1024; x++ ) 
    { 
      r = rand2rgb(rand());
      fwrite( &r, 3, 1, stdout );  // assume little endian. 
    }
}

Compilando e executando:

$ cc -O2 -o random random.c
$ ./random > rand1.ppm

Obtemos um arquivo rand1.ppm assim:

Ruído gerado por cores “aleatórias” dos pixels.

Recentemente vi alguém, num forum, tentando implementar seu próprio PRNG na rotina mcc_rnd(), abaixo:

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

// Scale n to 24 bit value (0xBBGGRR).
// OBS: Uses long double because 'long' is 64 bits long in x86-64 systems.
#define rand2rgb(n) \
  ( ( int ) ( ( (n) * 16777215.0L ) / LONG_MAX ) )

// using size_t instead of ptrdiff_t (same thing)
typedef size_t mcc_rnd_t;

static long mcc_rnd ( mcc_rnd_t *seed, long min, long max );

int main ( void )
{
  int x, y;
  int r;
  long seed = 1;

  fputs ( "P6\n1024 1024\n255\n", stdout );

  for ( y = 0; y < 1024; y++ )
    for ( x = 0; x < 1024; x++ ) 
    { 
      r = rand2rgb ( mcc_rnd ( &seed, 0, LONG_MAX ) ); 
      fwrite ( &r, 3, 1, stdout );
    }
}

long mcc_rnd ( mcc_rnd_t *seed, long min, long max )
{
  // Most of the time tmp will point to the SAME memory block
  // for every process. Notice that, probably, tmp is aligned,
  // meaning the lower bits aren't 'random' at all... 
  // and the upper bits isn't random as well...
  void *tmp = malloc ( 1 );

  long val = time ( NULL ) + ( size_t ) tmp;

  free ( tmp );

  // This isn't 'random' seed. Taking the argument address means 
  // it will be copied to the stack and the stack pointer isn't
  // that 'random'... The value on stack isn't that unpredictable
  // as you think!
  if ( ! seed )
    seed = ( mcc_rnd_t * ) ( &seed );

  // Well... this isn't random, again, isn't it?
  if ( ! *seed )
    *seed = 1;

  val %= *seed;
  val *= clock();

  // The seed, again, isn't random!
  *seed <<= 1; 

  // Saturation get even less random values! 
  return ( val > max ) ? max : ( val < min ? min : val );
}

Os comentários na função mcc_rnd() são meus. A rotina tem várias falhas:

  1. malloc() retorna um endereço no virtual address space, que tende a ser o mesmo para todos os processos;
  2. Dependendo da velocidade com que a rotina é chamada, time() e clock() retornam o mesmo valor;
  3. clock() não mede a quantidade de ciclos de clock do processador, mas tem uma granulidade muito maior. Mesmo que medisse, teríamos valores praticamente sequenciais;
  4. A semente (*seed), usada como divisor, é tudo menos aleatória;
  5. A “saturação”, no caso de valores “fora da faixa” torna a rotina não aleatória!

De fato, compilando e executando, obtemos uma imagem assim:

Opa! tyemos padrões previsíveis.

Além da maioria dos valores serem zero, os pixels coloridos seguem padrões específicos. Notem os muitos pixels vermelhos e os gradientes do verde para o azul nos “blocos” acima…

Bem… minha dica: Não tente inventar um PRNG na base da tentativa e erro ou assumindo que certos “fenômenos” são aleatórios…