O que há com 181783497276652981 e 8682522807148012 em Random (Java 7)?

112

Por que foram 181783497276652981e 8682522807148012escolhidos em Random.java?

Aqui está o código-fonte relevante do Java SE JDK 1.7:

/**
 * Creates a new random number generator. This constructor sets
 * the seed of the random number generator to a value very likely
 * to be distinct from any other invocation of this constructor.
 */
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static long seedUniquifier() {
    // L'Ecuyer, "Tables of Linear Congruential Generators of
    // Different Sizes and Good Lattice Structure", 1999
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);

Portanto, invocar new Random()sem qualquer parâmetro de semente leva o "uniquificador de semente" atual e executa o XOR com ele System.nanoTime(). Em seguida, ele usa 181783497276652981para criar outro uniquificador de semente para ser armazenado na próxima vez que new Random()for chamado.

Os literais 181783497276652981Le 8682522807148012Lnão são colocados em constantes, mas não aparecem em nenhum outro lugar.

A princípio, o comentário me dá uma pista fácil. A pesquisa online por aquele artigo produz o artigo real . 8682522807148012não aparece no papel, mas 181783497276652981aparece - como uma substring de outro número ,, 1181783497276652981que está 181783497276652981com um 1prefixado.

O artigo afirma que 1181783497276652981é um número que produz um bom "mérito" para um gerador de congruência linear. Este número foi simplesmente copiado incorretamente para o Java? Tem 181783497276652981um mérito aceitável?

E por que foi 8682522807148012escolhido?

A pesquisa online por qualquer um dos números não produz nenhuma explicação, apenas esta página que também mostra a queda 1na frente de 181783497276652981.

Poderiam ter sido escolhidos outros números que funcionassem tão bem quanto esses dois números? Por que ou por que não?

rgettman
fonte
Gostaria apenas de salientar que nenhuma das constantes mencionadas (mesmo as maiores com as iniciais) é muito grande para caber, embora a multiplicação certamente resultará em um estouro.
nanofarad de
6
8682522807148012é um legado da versão anterior da turma, como pode ser visto nas revisões feitas em 2010 . Na 181783497276652981Lverdade, parece ser um erro de digitação e você pode enviar um relatório de bug.
assylias
6
Ou é um erro de digitação, ou seja, um bug, ou um recurso com motivação não revelada. Você teria que perguntar aos autores. Qualquer coisa que você conseguir aqui será apenas uma opinião mais ou menos desinformada. Se você acha que é um bug, envie um relatório de bug.
Marquês de Lorne
1
Especialmente devido às diferentes respostas, isso poderia ser duas perguntas separadas para cada constante.
Mark Hurd de
1
É triste ver um gargalo de escalabilidade global embutido em uma classe tão fundamental. seedUniquifierpode ser extremamente disputado em uma caixa de 64 núcleos. Um thread local teria sido mais escalonável.
usr

Respostas:

57
  1. Este número foi simplesmente copiado incorretamente para o Java?

    Sim, parece ser um erro de digitação.

  2. 181783497276652981 tem um mérito aceitável?

    Isso pode ser determinado usando o algoritmo de avaliação apresentado no artigo. Mas o mérito do número "original" é provavelmente maior.

  3. E por que 8682522807148012 foi escolhido?

    Parece ser aleatório. Pode ser o resultado de System.nanoTime () quando o código foi escrito.

  4. Poderiam ter sido escolhidos outros números que funcionassem tão bem quanto esses dois números?

    Nem todos os números seriam igualmente "bons". Então não.

Estratégias de Semeadura

Existem diferenças no esquema de propagação padrão entre diferentes versões e implementação do JRE.

public Random() { this(System.currentTimeMillis()); }
public Random() { this(++seedUniquifier + System.nanoTime()); }
public Random() { this(seedUniquifier() ^ System.nanoTime()); }

O primeiro não é aceitável se você criar vários RNGs em uma linha. Se os tempos de criação caírem no mesmo intervalo de milissegundos, eles darão sequências completamente idênticas. (mesma semente => mesma sequência)

O segundo não é thread-safe. Vários threads podem obter RNGs idênticos ao inicializar ao mesmo tempo. Além disso, as sementes de inicializações subsequentes tendem a ser correlacionadas. Dependendo da resolução real do temporizador do sistema, a sequência de sementes pode ser linearmente crescente (n, n + 1, n + 2, ...). Conforme declarado em Quão diferentes as sementes aleatórias precisam ser? e o artigo referenciado Defeitos comuns na inicialização de geradores de número pseudo-aleatório , sementes correlacionadas podem gerar correlação entre as sequências reais de múltiplos RNGs.

A terceira abordagem cria sementes distribuídas aleatoriamente e, portanto, não correlacionadas, mesmo entre threads e inicializações subsequentes. Portanto, a documentação atual do java:

Este construtor define a semente do gerador de número aleatório para um valor que provavelmente será distinto de qualquer outra invocação desse construtor.

pode ser estendido por "entre threads" e "não correlacionado"

Qualidade da sequência de sementes

Mas a aleatoriedade da sequência de propagação é tão boa quanto o RNG subjacente. O RNG usado para a sequência de semente nesta implementação java usa um gerador congruencial linear multiplicativo (MLCG) com c = 0 e m = 2 ^ 64. (O módulo 2 ^ 64 é implicitamente dado pelo estouro de inteiros longos de 64 bits) Por causa do zero ce do módulo de potência de 2, a "qualidade" (comprimento do ciclo, correlação de bits, ...) é limitada . Como diz o artigo, além do comprimento total do ciclo, cada bit tem um comprimento de ciclo próprio, que diminui exponencialmente para bits menos significativos. Assim, os bits mais baixos têm um padrão de repetição menor. (O resultado de seedUniquifier () deve ser invertido em bits, antes de ser truncado para 48 bits no RNG real)

Mas é rápido! E para evitar loops de comparação e configuração desnecessários, o corpo do loop deve ser rápido. Isso provavelmente explica o uso desse MLCG específico, sem adição, sem xoragem, apenas uma multiplicação.

E o referido trabalho apresenta uma lista de bons "multiplicadores" para c = 0 em = 2 ^ 64, como 1181783497276652981.

Resumindo: A para o esforço @ JRE-developers;) Mas há um erro de digitação. (Mas quem sabe, a menos que alguém avalie, há a possibilidade de que o 1 líder ausente na verdade melhore o RNG de semeadura.)

Mas alguns multiplicadores são definitivamente piores: "1" leva a uma sequência constante. "2" leva a uma sequência de movimento de bit único (de alguma forma correlacionada) ...

A correlação inter-sequência para RNGs é realmente relevante para Simulações (Monte Carlo), onde várias sequências aleatórias são instanciadas e até mesmo paralelizadas. Portanto, uma boa estratégia de semeadura é necessária para obter execuções de simulação "independentes". Portanto, o padrão C ++ 11 introduz o conceito de uma sequência de sementes para gerar sementes não correlacionadas.

Thomas B.
fonte
3
Pelo menos ainda é estranho, se eles tivessem descartado o menos significativo em vez do mais significativo, então cada multiplicação perde um pouco até que eventualmente (após 62 passos) o seedUniquifierpare em zero.
Harold
9

Se você considerar que a equação usada para o gerador de números aleatórios é:

LCGEquation

Onde X (n + 1) é o próximo número, a é o multiplicador, X (n) é o número atual, c é o incremento e m é o módulo.

Se você olhar mais adiante Random, a, c e m são definidos no cabeçalho da classe

private static final long multiplier = 0x5DEECE66DL;   //= 25214903917 -- 'a'
private static final long addend = 0xBL;               //= 11          -- 'c'
private static final long mask = (1L << 48) - 1;       //= 2 ^ 48 - 1  -- 'm'

e olhando para o método em que protected int next(int bits)a equação é implementada

nextseed = (oldseed * multiplier + addend) & mask;
//X(n+1) =  (X(n)   *      a     +    c  ) mod m

Isso implica que o método seedUniquifier() está realmente obtendo X (n) ou no primeiro caso na inicialização X (0) que é 8682522807148012 * 181783497276652981, na verdade , este valor é então modificado pelo valor de System.nanoTime(). Este algoritmo é consistente com a equação acima, mas com o seguinte X (0) = 8682522807148012, a = 181783497276652981, m = 2 ^ 64 e c = 0. Mas como o mod m de é pré-formado pelo longo estouro, a equação acima se torna

eq2

Olhando para o papel , o valor de a = 1181783497276652981é para m = 2 ^ 64, c = 0. Portanto, parece ser apenas um erro de digitação e o valor 8682522807148012de X (0), que parece ser um número aparentemente escolhido aleatoriamente do código legado para Random. Como visto aqui. Mas o mérito desses números escolhidos ainda pode ser válido, mas como mencionado por Thomas B. provavelmente não tão "bom" quanto o do papel.

EDITAR - Os pensamentos abaixo originais foram esclarecidos, então podem ser desconsiderados, mas deixando-os para referência

Isso me leva às conclusões:

  1. A referência ao artigo não é para o valor em si, mas para os métodos usados ​​para obter os valores devido aos diferentes valores de a, c e m

  2. É mera coincidência que o valor seja o mesmo, exceto o 1 inicial e o comentário esteja fora do lugar (embora ainda esteja lutando para acreditar nisso)

OU

Houve um sério mal-entendido sobre as tabelas no papel e os desenvolvedores apenas escolheram um valor aleatoriamente quando ele é multiplicado, qual era o objetivo de usar o valor da tabela em primeiro lugar, especialmente porque você pode apenas fornecer seu próprio valor de semente de qualquer maneira, caso em que esses valores nem são levados em consideração

Então, para responder sua pergunta

Poderiam ter sido escolhidos outros números que funcionassem tão bem quanto esses dois números? Por que ou por que não?

Sim, qualquer número poderia ter sido usado, na verdade, se você especificar um valor de semente quando instanciar Random, você está usando qualquer outro valor. Este valor não tem nenhum efeito no desempenho do gerador, ele é determinado pelos valores de a, c e m que são codificados dentro da classe.

Devil Java
fonte
1
Na verdade, não - Existem dois algoritmos: (i) 1 para criar uma nova semente aleatória sempre que o construtor é chamado. Esse algo usa um simples X_n + 1 = X_n * a. Por causa do overflow longo, isso é equivalente a X_n + 1 = X_n * a mod m. Com a = 181783497276652981 e m = 2 ^ 64. (ii) Outro algo, que, partindo de uma dada semente, produz uma série de números aleatórios. Esse segundo algoritmo é o que você menciona e os documentos explicam que " Este é um gerador de números pseudo-aleatórios congruencial linear, conforme descrito por Knuth em The Art of Computer Programming ".
assylias
1
@assylias Entendo seu ponto, fiquei tão preso ao código-fonte Randome ao artigo citado que ultrapassou completamente a questão original, irei editar em breve, obrigado.
Java Devil
3

De acordo com o link que você forneceu, eles escolheram ( depois de adicionar o 1 :) que faltava ) o melhor rendimento de 2 ^ 64 porque long não pode ter um número de 2 ^ 128

Jaffar Ramay
fonte