- O crate de números aleatórios mais conhecido do Rust,
rand, espalha operações do dia a dia por vários traits, então foi criado o urandom com uma superfície pública e de implementação menor e uma experiência de uso mais consistente - As operações de alto nível foram reunidas em uma única struct
Random, e o traitRngfoi selado, priorizando descoberta da API e otimizações internas em vez de suporte a geradores arbitrários - Sem introduzir novos algoritmos de números aleatórios, as funções de saída do Xoshiro256 foram escolhidas conforme o uso, registrando cerca de 31% mais throughput do que
rand0.10.2 em um benchmark gerando 1.000 valoresf64 - A amostragem uniforme de inteiros foi unificada para caminhos reutilizáveis e pontuais com uma única implementação sem viés que calcula o limiar de forma preguiçosa; no benchmark do intervalo
500..20_000, foi mais rápida que os dois caminhos dorand - A saída bruta com seed explícita garante reprodutibilidade nas arquiteturas suportadas e em releases compatíveis com SemVer, mas abre mão da conexão de geradores arbitrários e do amplo ecossistema de distribuições e integrações de terceiros do
rand
API Random reunida em um só lugar
- As operações úteis do
randestão espalhadas por vários traits- Para gerar números em intervalos aleatórios é preciso
RngExt, para escolher itens em sequências,IndexedRandom, e para embaralhar,SliceRandom - O
rand0.10 fornece helpers no nível da raiz, comorand::random_range, para chamadas pontuais - Mas, para manter um handle de RNG ou usar operações de sequência como seleção e embaralhamento, ainda é preciso encontrar métodos em vários traits
- Para gerar números em intervalos aleatórios é preciso
- Mesmo reduzindo imports com o prelude, ainda é necessário saber a que tipo os métodos de extensão se aplicam — RNG, slice ou iterador — então é difícil encontrá-los apenas com autocomplete da IDE
- O urandom coloca a API consumidora de alto nível em uma única struct wrapper
Randomurandom::new()cria umRandom<urandom::rng::Xoshiro256Rng>- É possível chamar
uniform,chooseeshuffleno mesmo objeto - O autocomplete mostra
random,uniform,chance,choose,shuffle,sampleetc. - Como todos são métodos próprios, não é necessário encontrar nem importar traits de extensão de alto nível
Rng selado: otimização em vez de extensibilidade
- O
randtrata traits de RNG de baixo nível como pontos públicos de extensão, mas nourandomo traitRngé selado, e os geradores suportados são escolhidos e implementados dentro do próprio crate- Não é possível conectar um gerador arbitrário ao
Random - Para adicionar um novo gerador, é preciso alterar o próprio
urandom
- Não é possível conectar um gerador arbitrário ao
- Se o objetivo for um algoritmo melhor, Xoshiro256 e ChaCha já se consolidaram como escolhas padrão por função, e as recomendações mudam devagar
- Se surgir uma opção melhor, ela pode ser adotada em um futuro major release
- Para compatibilidade com outros projetos, linguagens de programação, algoritmos legados, hardware especial ou geradores exclusivos para simulação, não basta ter o mesmo gerador
- Algoritmos relacionados, como amostragem uniforme e embaralhamento, também precisam ser iguais, então uma implementação dedicada que cubra o contrato inteiro é mais adequada
- Graças ao trait selado, o
urandompode adicionar apenas as operações brutas de que precisa sem projetar nem documentar um contrato de implementação para geradores desconhecidos e casos excepcionais- Isso permite especializar geradores e algoritmos em conjunto, viabilizando algumas otimizações que o
randnão pode usar
- Isso permite especializar geradores e algoritmos em conjunto, viabilizando algumas otimizações que o
- Na maioria das aplicações, escolha de entropia é mais útil do que uma nova implementação de PRNG
- Geradores concretos expõem construtores nativos
from_seed - É possível criar um
Randomcom seed explícita, comoChaCha12Rng::from_seed(seed) - Ele não aceita implementações arbitrárias de RNG, mas preserva os pontos de extensão que usuários avançados provavelmente precisam
- Geradores concretos expõem construtores nativos
Ganhos de desempenho com os mesmos algoritmos
- O
urandomnão usa novos algoritmos de geração de números aleatórios- Em sistemas 64-bit,
urandom::new()para uso não criptográfico erand::rngs::SmallRngusam a mesma família Xoshiro256 - Para uso criptográfico,
urandom::csprng()erand::rngs::StdRngusam ChaCha12 - O gerador interno da função de conveniência
rand::rng()também é ChaCha12
- Em sistemas 64-bit,
- A interface de geradores do
randfornece palavras inteiras e preenchimento de bytes, então até distribuições que precisam def64acabam pedindo umu64inteiro primeiro - O
urandom::Rngoferece não sónext_u32enext_u64, mas tambémnext_f32enext_f64- Números aleatórios de ponto flutuante exigem menos bits aleatórios do que uma palavra inteira
- O gerador pode redefinir esses métodos com funções de saída mais baratas
- A implementação do Xoshiro compartilha a mesma transição de estado, mas separa os caminhos de saída
- Para
u64, mantém Xoshiro256++ - Para
u32e ponto flutuante, usa o mais rápido Xoshiro256+, cujos bits superiores foram pensados para esses usos
- Para
- Os resultados dos microbenchmarks gerando 1.000 números aleatórios com
urandom1.0 erand0.10.2 foram os seguintes- Xoshiro
u64: ambos 814ns - Xoshiro
u32:rand836ns,urandom788ns - Xoshiro
f64:rand1.033ns,urandom788ns - ChaCha12
f64:rand2.199ns,urandom2.011ns
- Xoshiro
- O throughput de ponta a ponta para Xoshiro
f64foi cerca de 31% maior e o tempo de execução 24% menor, enquanto o caminhou64, que faz o mesmo trabalho, ficou praticamente empatado - Como o ChaCha12 não redefine
next_f64, o desempenho ficou em grande parte parecido - Os tempos exatos variam conforme máquina e compilador; as condições detalhadas estão nas notas completas de benchmark
Caminho de amostragem uniforme unificado
- Aplicar uma operação simples de resto ao comprimento de um intervalo inteiro introduz viés, então uma amostragem uniforme correta de inteiros precisa rejeitar parte da saída do gerador
- Calcular o limiar exato de rejeição exige uma operação de resto cara
- Se o amostrador for reutilizado repetidamente, isso pode ser amortizado como custo de setup inicial
- Ao gerar apenas um valor, esse custo se torna relativamente alto
- O
randexpõe essa diferença por meio do traitUniformSampler- O
UniformIntconstruído pré-calcula o limiar para amostrar sem viés Rng::random_rangeusa hooks separadossample_singleousample_single_inclusivepara evitar o custo de setup- Na funcionalidade padrão, o caminho rápido pontual usa um segundo algoritmo levemente enviesado
- O recurso opcional
unbiasedsubstitui isso por uma versão iterativa mais complexa
- O
- O
urandomcalcula o limiar de forma preguiçosa e usa uma única implementação sem viés baseada em multiplicação e rejeição tanto para intervalos reutilizados quanto para usos pontuais- Segue a abordagem descrita no artigo de 2018 de Daniel Lemire, Fast Random Integer Generation in an Interval
- Na maioria dos intervalos práticos, o primeiro candidato é retornado antes da divisão
- Se o primeiro candidato não puder ser retornado, o limiar exato é calculado e a repetição continua sem viés
- Também trata a exceção
range == 0, quando o intervalo inteiro é solicitado
- A mesma implementação atende distribuições reutilizadas e intervalos pontuais, sem método separado, segundo algoritmo, custo de setup antecipado nem caminho rápido enviesado
- No benchmark extraindo 1.000 valores do intervalo
500..20_000, os resultados foram os seguintesUniformIntreutilizado:rand1.098ns,urandom950ns- Intervalo pontual:
rand1.079ns,urandom942ns
- Os resultados do
randconsideram a funcionalidade padrão, então até a linha pontual mais rápida usa um caminho levemente enviesado, enquanto ourandomfoi mais rápido que ambos mantendo ausência de viés
Reprodutibilidade entre releases e arquiteturas
- O
urandomtrata reprodutibilidade como parte do contrato público- Com a mesma seed explícita e a mesma sequência de chamadas de RNG de baixo nível, a saída bruta de um gerador determinístico é preservada
- A estabilidade é garantida em todas as arquiteturas suportadas e releases compatíveis com SemVer
- Um servidor 64-bit e um cliente WebAssembly 32-bit podem usar a mesma base de gerador para replay
- Para manter essa compatibilidade, ele sacrifica desempenho em arquiteturas 32-bit
- Trata-se de uma garantia mais forte do que a política de reprodutibilidade do
rand- A saída dos geradores portáveis e dos algoritmos de amostragem do
randpode mudar em minor releases SmallRngeStdRngexplicitamente não são portáveis e podem mudar conforme a plataforma ou o release da biblioteca
- A saída dos geradores portáveis e dos algoritmos de amostragem do
O custo da escolha e quando usar
- O
urandomreúne operações comuns emRandom, tornando-as fáceis de encontrar sem traits de extensão - Ele projeta geradores e distribuições em conjunto para implementar caminhos de saída Xoshiro mais baratos e um único caminho de amostragem uniforme sem viés
- O fluxo bruto estável de geradores com seed explícita pode ser usado em jogos determinísticos e simulações
- Em troca, não é possível trazer geradores arbitrários, e ele também não oferece a lista maior de distribuições nem o ecossistema de integrações de terceiros do
rand - Se você precisa de um ecossistema amplo, o
randé a opção certa; se prefere uma superfície de API pequena, boa descoberta, otimizações integradas e uma política forte de reprodutibilidade, pode escolher ourandom - O pacote pode ser encontrado em crates.io, na documentação da API e no código-fonte no GitHub
1 comentários
Comentários no Lobste.rs
Há bons motivos para fazer um fork do
rand, mas o nomeurandomsoa como uma biblioteca relacionada a/dev/urandomConcordo com a preocupação, mas não gosto de
pub fn new() -> Random<impl Rng + Clone>Parametrizar a aplicação inteira como
Random<T> where T: Rngaumenta o trabalho chato, e os problemas de tempo de compilação e relacionados adynficam sérios. Eu preferiria questruct Randomtivesse um tipo concreto ou, como segunda opção, usariastruct Random<T = rng::Xoshiro256Rng>Por frustrações parecidas, eu mesmo já fiz algo, mas não é um fork e tem muito menos recursos que o
randFico feliz que alguém que sentia o mesmo problema que eu tenha realmente tentado resolvê-lo. O Rust parece ter uma tendência estranha a levar as pessoas a criarem bibliotecas de sopa de traits
O tipo de dado central do banco de dados com que trabalho precisa implementar pelo menos 15 traits, então o autocompletar fica uma bagunça e a documentação também confunde. Reduzimos um pouco o número de traits, mas frequentemente esbarramos em dependências circulares ou na impossibilidade de escrever testes essenciais
Esta biblioteca me lembra tanto as interfaces profundas de APOSD quanto o trabalho de criptografia do Filippo projetado para ser difícil de usar errado, e ambos são grandes elogios
urandom::new()não retorna um gerador de números aleatórios criptograficamente seguro, então o design não é totalmente à prova de erro. Isso fica ainda mais confuso porque, no Linux,/dev/urandomé seguroOutra alternativa ao
randé ofastrand, um gerador de números aleatórios simples e rápido. Ele é mais simples querandeurandom, mas também tem menos recursos