¡Hola a todos! Esta vez no es tanto una duda, sino un pedido a que me aclaren alguito de lo que estoy "investigando".
Resulta que entre mis pruebas a algunos módulos de mis proyectos para generar números para probar cálculos empleaba la función Random.
Lo que me llamó la atención es que en cada vez que ejecutaba y hacía pruebas veía que me mostraba diferentes números.
Uno me dirá, pues claro, porque eso es lo que hace random. Pues si y no. Eso es el resultado esperado mientras se invoque inicialmente a Randomize. De otra forma SIEMPRE debería obtenerse la misma secuencia. Y justamente en ninguna parte del código había puesto Randomize.
Mi teoría entonces pasó a que la función Random que posee Lazarus no tiene un valor fijo de semilla. Asi que hice una proyecto en limpio para probar muy simple que estaba pasando. El proyecto contiene simplemente esto:
procedure TForm1.Button1Click(Sender: TObject); var rn: integer; begin rn := trunc(random * 255); showMessage(IntToStr(rn)); end; procedure TForm1.Button2Click(Sender: TObject); var rn: double; begin rn := random; showMessage(FloatToStr(rn)); end;
Y empecé a jugar... Cierro la aplicación y ejecuto de nuevo. Para mi sorpresa, volvía a dar los mismos números... derrumbando mi teoría.
Algo no me cuadraba... Entonces vi el porqué de mi rareza. Volví a ejecutar la aplicación y ahora empecé a alternar entre el botón 1 y 2 y ahora asi la secuencia se rompía. Descubrí así que no es que yo había escrito mal o tenía algo raro en mi aplicación.
Simplemente se debía a que estaba generando números en diferentes circunstancias de modo que mientras no respete ese mismo orden/secuencia de acciones no tendría el mismo resultado esperado. Todo porque invoco a random desde varios puntos.
Pero algo me seguía picando la curiosidad. ¿Cómo es la función random de Lazarus? ¿Cuál es el valor de semilla? Sabía que tenía una implementación diferente a la de Delphi, que hasta el momento y tengo entendido es un generador congruencial mixto/multiplicativo, pero no había visto nada sobre el tema. Me voy a leer la ayuda y encuentro esto:
The Free Pascal implementation of the Random routine uses the Mersenne Twister to simulate randomness. This implementation has a better statistical distribution than for example a Linear Congruential generator algorithm, but is considerably slower than the latter. If speed is an issue, then alternate random number generators should be considered.
¿Primos Mersenne? Interesante.
Los primos Mersenne para mi son conocidos, y si, se que los generadores utilizan números primos para obtener los números pseudoaleatorios. Lo que más me llama la forma en como se los aplica. Todo un mundo nuevo.
Leyendo en la wiki sobre esta técnica me doy con que es una implementación mucho más "académica", y mucho más pseudoaleatoria que los generadores congruenciales clásicos. Su contra: es más lento y para secuencias muy largas o en contextos que se va estar invocando generando miles de veces no es la mejor alternativa.
Como quien dice, ¡Todo los días se aprende algo nuevo!
Ya necesita ver esto de cerca, asi que me voy al código de random y me doy con el monstruo:
{$ifndef FPUNONE} function random: extended; begin random := cardinal(genrand_MT19937) * (extended(1.0)/(int64(1) shl 32)); end; {$endif} {$endif FPC_HAS_FEATURE_RANDOM}
Ummm veamos ese bicho de genrand_MT19937:
function genrand_MT19937: longint; const mag01 : array [0..1] of longint =(0, longint(MT19937MATRIX_A)); var y: longint; kk: longint; begin if RandSeed<>OldRandSeed then mti:=MT19937N+1; if (mti >= MT19937N) { generate MT19937N longints at one time } then begin if mti = (MT19937N+1) then // if sgenrand_MT19937() has not been called, begin sgenrand_MT19937(randseed); // default initial seed is used { hack: randseed is not used more than once in this algorithm. Most } { user changes are re-initialising reandseed with the value it had } { at the start -> with the "not", we will detect this change. } { Detecting other changes is not useful, since the generated } { numbers will be different anyway. } randseed := not(randseed); oldrandseed := randseed; end; for kk:=0 to MT19937N-MT19937M-1 do begin y := (mt[kk] and MT19937UPPER_MASK) or (mt[kk+1] and MT19937LOWER_MASK); mt[kk] := mt[kk+MT19937M] xor (y shr 1) xor mag01[y and $00000001]; end; for kk:= MT19937N-MT19937M to MT19937N-2 do begin y := (mt[kk] and MT19937UPPER_MASK) or (mt[kk+1] and MT19937LOWER_MASK); mt[kk] := mt[kk+(MT19937M-MT19937N)] xor (y shr 1) xor mag01[y and $00000001]; end; y := (mt[MT19937N-1] and MT19937UPPER_MASK) or (mt[0] and MT19937LOWER_MASK); mt[MT19937N-1] := mt[MT19937M-1] xor (y shr 1) xor mag01[y and $00000001]; mti := 0; end; y := mt[mti]; inc(mti); y := y xor (y shr 11); y := y xor (y shl 7) and TEMPERING_MASK_B; y := y xor (y shl 15) and TEMPERING_MASK_C; y := y xor (y shr 18); Result := y; end;
Lo que me preocupa es justamente los comentarios: ¿Hack? ¿Es vulnerable? ¡Mi no entender!
El código inicia según cierto valor de semilla, se ve claro en sgenrand_MT19937(randseed); pero el asunto es que en ningún momento se da algún valor a este parámetro. De hecho randSeed es una variable global que en ningún momento se establece valor alguno, que no sea en esta función (cuando hace randseed := not(randseed)) o en en Randomize:
procedure randomize; begin randseed:=GetTickCount; end;
Por lo que su valor, al igual que oldRandSeed (otra variable global) inicialmente tienen en valor 0.
¡Que alguien me explique!
Saludos,