Saltar al contenido principal

Hay `Math.random()`, y luego hay `Math.random()`

· 5 min de lectura
Yang Guo ([@hashseed](https://twitter.com/hashseed)), ingeniero de software y diseñador de dados

Math.random() devuelve un valor de tipo Number con signo positivo, mayor o igual a 0 pero menor que 1, elegido aleatoriamente o pseudo-aleatoriamente con una distribución aproximadamente uniforme en ese rango, utilizando un algoritmo o estrategia dependiente de la implementación. Esta función no toma argumentos.

ES 2015, sección 20.2.2.27

Math.random() es la fuente de aleatoriedad más conocida y utilizada frecuentemente en JavaScript. En V8 y la mayoría de otros motores de JavaScript, se implementa usando un generador de números pseudoaleatorios (PRNG). Como ocurre con todos los PRNGs, el número aleatorio se deriva de un estado interno que se modifica mediante un algoritmo fijo para cada nuevo número aleatorio. Entonces, para un estado inicial dado, la secuencia de números aleatorios es determinista. Dado que el tamaño en bits n del estado interno está limitado, los números que genera un PRNG eventualmente se repiten. El límite superior para la longitud del periodo de este ciclo de permutación es 2n.

Existen muchos algoritmos PRNG diferentes; entre los más conocidos están Mersenne-Twister y LCG. Cada uno tiene sus características particulares, ventajas y desventajas. Idealmente, debería usar la menor cantidad de memoria posible para el estado inicial, ser rápido de ejecutar, tener una longitud de periodo grande y ofrecer una distribución aleatoria de alta calidad. Mientras que el uso de memoria, el rendimiento y la longitud del periodo se pueden medir o calcular fácilmente, la calidad es más difícil de determinar. Hay muchas matemáticas detrás de las pruebas estadísticas para verificar la calidad de los números aleatorios. El estándar de facto de las pruebas PRNG, TestU01, implementa muchas de estas pruebas.

Hasta finales de 2015 (hasta la versión 4.9.40), la elección de PRNG de V8 era MWC1616 (multiplica con transporte, combinando dos partes de 16 bits). Usa 64 bits de estado interno y se ve aproximadamente así:

uint32_t state0 = 1;
uint32_t state1 = 2;
uint32_t mwc1616() {
state0 = 18030 * (state0 & 0xFFFF) + (state0 >> 16);
state1 = 30903 * (state1 & 0xFFFF) + (state1 >> 16);
return state0 << 16 + (state1 & 0xFFFF);
}

El valor de 32 bits se convierte luego en un número de punto flotante entre 0 y 1 de acuerdo con la especificación.

MWC1616 usa poca memoria y es bastante rápido de calcular, pero desafortunadamente ofrece una calidad inferior:

  • La cantidad de valores aleatorios que puede generar está limitada a 232, en lugar de los 252 números entre 0 y 1 que el punto flotante de doble precisión puede representar.
  • La mitad más significativa del resultado depende casi por completo del valor de state0. La longitud del periodo sería como máximo 232, pero en lugar de unos pocos ciclos de permutación grandes, hay muchos cortos. Con un estado inicial mal elegido, la longitud del ciclo podría ser menos de 40 millones.
  • Falla muchas pruebas estadísticas en el conjunto de pruebas TestU01.

Esto nos fue señalado, y tras comprender el problema y realizar algunas investigaciones, decidimos reimplementar Math.random basándonos en un algoritmo llamado xorshift128+. Usa 128 bits de estado interno, tiene una longitud de periodo de 2128 - 1 y pasa todas las pruebas del conjunto TestU01.

La implementación aterrizó en V8 v4.9.41.0 pocos días después de que fuéramos conscientes del problema. Se volvió disponible con Chrome 49. Tanto Firefox como Safari también cambiaron a xorshift128+.

En V8 v7.1, la implementación se ajustó nuevamente CL dependiendo únicamente de state0. Por favor, encuentren más detalles de la implementación en el código fuente.

No te equivoques: aunque xorshift128+ es una gran mejora con respecto a MWC1616, todavía no es criptográficamente seguro. Para casos de uso como hash, generación de firmas y cifrado/descifrado, los PRNG ordinarios no son adecuados. La API de Criptografía Web introduce window.crypto.getRandomValues, un método que devuelve valores aleatorios criptográficamente seguros, a un costo de rendimiento.

Ten en cuenta, si encuentras áreas de mejora en V8 y Chrome, incluso aquellas que — como esta — no afectan directamente el cumplimiento de especificaciones, la estabilidad o la seguridad, por favor reporta un problema en nuestro rastreador de errores.