1 puntos por GN⁺ 2 시간 전 | 1 comentarios | Compartir por WhatsApp
  • rand, el crate de números aleatorios más representativo de Rust, reparte operaciones cotidianas entre varios traits, por lo que se desarrolló urandom, con una superficie pública y de implementación más pequeña y una experiencia de uso coherente
  • Agrupa las operaciones de alto nivel en una sola estructura Random y sella el trait Rng, priorizando la descubribilidad de la API y las optimizaciones internas por encima del soporte para generadores arbitrarios
  • Sin introducir nuevos algoritmos de números aleatorios, elige la función de salida de Xoshiro256 según el uso, y en un benchmark de generación de 1,000 valores f64 registró un rendimiento aproximadamente 31% mayor que rand 0.10.2
  • El muestreo uniforme de enteros unifica las rutas reutilizables y de un solo uso con una única implementación sin sesgo que calcula el umbral de forma diferida, y en el benchmark del rango 500..20_000 fue más rápida que las dos rutas de rand
  • La salida bruta con semilla explícita garantiza reproducibilidad en arquitecturas compatibles y releases compatibles con SemVer, pero renuncia a la conexión de generadores arbitrarios y al vasto ecosistema de distribuciones e integraciones de terceros de rand

API Random unificada

  • Las operaciones útiles de rand están repartidas entre varios traits
    • Para generar rangos aleatorios se necesita RngExt, para elegir secuencias IndexedRandom, y para mezclar SliceRandom
    • rand 0.10 ofrece helpers a nivel raíz como rand::random_range para llamadas puntuales
    • Si se mantiene un handle de RNG o se usan operaciones de secuencia como elegir o mezclar, todavía hay que encontrar métodos de varios traits
  • Aunque el prelude reduce los imports, sigue siendo necesario saber a qué tipo se aplican los métodos de extensión —RNG, slices o iteradores—, por lo que son difíciles de descubrir solo con el autocompletado del IDE
  • urandom coloca la API de consumo de alto nivel en una sola estructura wrapper Random
    • urandom::new() crea Random<urandom::rng::Xoshiro256Rng>
    • uniform, choose y shuffle pueden llamarse desde el mismo objeto
    • El autocompletado permite ver random, uniform, chance, choose, shuffle, sample, entre otros
    • Como todos son métodos propios, no hace falta encontrar ni importar traits de extensión de alto nivel

Un Rng sellado que elige optimización en lugar de extensibilidad

  • rand trata los traits de RNG de bajo nivel como puntos de extensión públicos, pero el trait Rng de urandom está sellado, de modo que los generadores compatibles se eligen e implementan dentro del crate
    • No se pueden conectar generadores arbitrarios a Random
    • Para agregar un nuevo generador hay que modificar el propio urandom
  • Si el objetivo es un mejor algoritmo, las opciones predeterminadas por rol ya están ocupadas por Xoshiro256 y ChaCha, y las recomendaciones también cambian lentamente
    • Si aparece una mejor opción, puede adoptarse en un futuro release mayor
  • Para compatibilidad con otros proyectos, lenguajes de programación, algoritmos heredados, hardware especial o generadores dedicados a simulaciones, no basta con que el generador sea el mismo
    • También deben coincidir algoritmos relacionados como el muestreo uniforme y el mezclado, por lo que conviene más una implementación dedicada que implemente todo el contrato
  • Gracias al trait sellado, se pueden agregar solo las operaciones primitivas que urandom necesita, sin diseñar ni documentar contratos de implementación para generadores desconocidos y casos excepcionales
    • Se pueden especializar generadores y algoritmos para que encajen entre sí, lo que permite algunas optimizaciones que no están disponibles en rand
  • En la mayoría de las aplicaciones, elegir la entropía es más útil que implementar un nuevo PRNG
    • Los generadores concretos exponen constructores nativos from_seed
    • Se puede crear un Random con una semilla explícita, como ChaCha12Rng::from_seed(seed)
    • No acepta implementaciones arbitrarias de RNG, pero conserva los puntos de extensión que se espera que necesiten los usuarios avanzados

Mejoras de rendimiento obtenidas con el mismo algoritmo

  • urandom no usa un nuevo algoritmo de generación de números aleatorios
    • En sistemas de 64 bits, urandom::new() para usos no criptográficos y rand::rngs::SmallRng usan la misma familia Xoshiro256
    • Para usos criptográficos, urandom::csprng() y rand::rngs::StdRng usan ChaCha12
    • El generador interno de la función de conveniencia rand::rng() también es ChaCha12
  • La interfaz de generador de rand proporciona palabras enteras y llenado de bytes, por lo que incluso las distribuciones que necesitan f64 solicitan primero un u64 completo
  • urandom::Rng proporciona no solo next_u32 y next_u64, sino también next_f32 y next_f64
    • Los números aleatorios de punto flotante necesitan menos bits aleatorios que una palabra completa
    • Los generadores pueden sobrescribir estos métodos con funciones de salida más baratas
  • La implementación de Xoshiro comparte la transición de estado, pero separa las rutas de salida
    • Para u64 mantiene Xoshiro256++
    • Para u32 y punto flotante usa el Xoshiro256+ más rápido, cuyos bits altos están diseñados para esos usos
  • Los resultados de microbenchmarks que generaron 1,000 números aleatorios con urandom 1.0 y rand 0.10.2 fueron los siguientes
    • Xoshiro u64: 814ns en ambos
    • Xoshiro u32: rand 836ns, urandom 788ns
    • Xoshiro f64: rand 1,033ns, urandom 788ns
    • ChaCha12 f64: rand 2,199ns, urandom 2,011ns
  • El rendimiento de extremo a extremo de Xoshiro f64 fue aproximadamente 31% mayor y el tiempo de ejecución 24% menor, pero la ruta u64, que realiza la misma tarea, estuvo prácticamente empatada
  • ChaCha12 no sobrescribe next_f64, por lo que su rendimiento es en general similar
  • Los tiempos exactos varían según la máquina y el compilador; las condiciones detalladas pueden consultarse en las notas completas del benchmark

Una única ruta integrada para el muestreo uniforme

  • Si a los enteros se les aplica simplemente el operador módulo con la longitud del rango, aparece sesgo, por lo que el muestreo uniforme correcto de enteros debe rechazar parte de la salida del generador
  • Calcular el umbral exacto de rechazo requiere una operación de módulo costosa
    • Si el muestreador se reutiliza, puede asumirse como costo inicial de configuración
    • Cuando solo se genera un valor, ese costo pesa relativamente más
  • rand expone esta diferencia mediante el trait UniformSampler
    • El UniformInt generado calcula el umbral por adelantado para muestrear sin sesgo
    • Rng::random_range usa hooks separados sample_single o sample_single_inclusive para evitar la configuración inicial
    • Con las funciones predeterminadas, la ruta rápida de un solo uso usa un segundo algoritmo ligeramente sesgado
    • La función opcional unbiased lo reemplaza por una versión iterativa más compleja
  • urandom usa una única implementación de multiplicación y rechazo sin sesgo tanto para rangos reutilizables como de un solo uso, calculando el umbral de forma diferida
    • Sigue el método descrito en el artículo de 2018 de Daniel Lemire, Fast Random Integer Generation in an Interval
    • En la mayoría de los rangos prácticos, el primer candidato se devuelve antes de cualquier división
    • Si el primer candidato no puede devolverse, se calcula el umbral exacto y luego se repite sin sesgo
    • También maneja la excepción range == 0, cuando se solicita el rango completo
  • La misma implementación maneja distribuciones reutilizables y rangos de un solo uso, sin métodos separados, sin un segundo algoritmo, sin costo de configuración anticipado y sin ruta rápida sesgada
  • Los resultados del benchmark al extraer 1,000 valores del rango 500..20_000 fueron los siguientes
    • UniformInt reutilizable: rand 1,098ns, urandom 950ns
    • Rango de un solo uso: rand 1,079ns, urandom 942ns
  • Como los resultados de rand son con las funciones predeterminadas, la fila más rápida de un solo uso corresponde a una ruta ligeramente sesgada, mientras que urandom fue más rápido que ambas rutas manteniéndose sin sesgo

Reproducibilidad entre releases y arquitecturas

  • urandom trata la reproducibilidad como parte de su contrato público
    • Dada la misma semilla explícita y el mismo orden de llamadas al RNG de bajo nivel, se conserva la salida bruta de los generadores deterministas
    • Garantiza estabilidad en arquitecturas compatibles y releases compatibles con SemVer
    • Un servidor de 64 bits y un cliente WebAssembly de 32 bits pueden usar la misma base de generador para reproducción
  • Para mantener esta compatibilidad, se sacrifica rendimiento en arquitecturas de 32 bits
  • Es una garantía más fuerte que la política de reproducibilidad de rand
    • Los generadores portables y algoritmos de muestreo de rand pueden cambiar su salida en releases menores
    • SmallRng y StdRng no son explícitamente portables y también pueden cambiar según la plataforma o el release de la biblioteca

Costos de la elección y criterios de adopción

  • urandom reúne las operaciones comunes en Random para que sean fáciles de encontrar sin traits de extensión
  • Diseña juntos generadores y distribuciones para implementar rutas de salida Xoshiro más baratas y una única ruta de muestreo uniforme sin sesgo
  • El stream bruto estable de generadores con semilla explícita puede aprovecharse en juegos deterministas y simulaciones
  • A cambio, no se pueden incorporar generadores arbitrarios y tampoco cuenta con la lista más amplia de distribuciones ni el ecosistema de integraciones de terceros que ofrece rand
  • Si se necesita un ecosistema amplio, rand es la opción adecuada; si se prefiere una superficie de API pequeña, descubribilidad, optimizaciones integradas y una política fuerte de reproducibilidad, se puede elegir urandom
  • El paquete está disponible en crates.io, la documentación de la API y el código fuente en GitHub

1 comentarios

 
GN⁺ 2 시간 전
Opiniones en Lobste.rs
  • Hay razones de sobra para hacer un fork de rand, pero el nombre urandom suena como una librería relacionada con /dev/urandom

    • Parece útil, pero el nombre puede prestarse a confusión. Si hubiera visto solo el nombre sin leer el artículo, probablemente no le habría prestado atención porque habría pensado que depende de entrada/salida de archivos
  • Estoy de acuerdo con el planteamiento del problema, pero no me convence pub fn new() -> Random<impl Rng + Clone>
    Parametrizar toda la aplicación como Random<T> where T: Rng agrega trabajo engorroso, y los problemas de tiempo de compilación y los relacionados con dyn se vuelven serios. Preferiría que struct Random tuviera un tipo concreto, o como segunda opción struct Random<T = rng::Xoshiro256Rng>

  • Ya había hecho algo por mi cuenta por una frustración parecida, aunque no fue un fork, y tiene muchas menos funciones que rand

  • Me da gusto que alguien que sintió el mismo problema que yo de verdad se haya puesto a resolverlo. Rust parece tener una tendencia extraña a empujar a la gente a crear librerías de sopa de traits
    Los tipos de datos principales de la base de datos con la que trabajo necesitan implementar al menos 15 traits, así que el autocompletado es un desastre y la documentación también es confusa. He reducido un poco la cantidad de traits, pero a menudo termino bloqueado por problemas como dependencias circulares o por no poder escribir pruebas clave

    • Es un fenómeno causado por astronautas de arquitectura que vienen de Java y aplican el mismo estilo orientado a objetos en Rust. Las dependencias circulares son una señal de que intentaste dividir a la fuerza algo que todavía no se puede separar, o de que no lograste distinguir correctamente tres objetos distintos. Si tienes control total del código, puedes usar enums en lugar de traits
    • En el ecosistema criptográfico de Rust, el problema de la sopa de traits es especialmente grave; es desesperante
  • Esta librería me recuerda tanto a las interfaces profundas de APOSD como al trabajo de criptografía de Filippo diseñado para que sea difícil equivocarse, y ambos son grandes elogios

    • Aun así, urandom::new() no devuelve un generador de números aleatorios criptográficamente seguro, así que no es un diseño completamente a prueba de errores. Eso lo hace más confuso todavía, sobre todo porque /dev/urandom en Linux sí es seguro
  • Como otra alternativa a rand, está fastrand, un generador de números aleatorios simple y rápido. Es más sencillo que rand y urandom, pero también tiene menos funciones