Qu'est-ce que 181783497276652981 et 8682522807148012 en aléatoire (Java 7)?

112

Pourquoi ont été 181783497276652981et 8682522807148012choisis Random.java?

Voici le code source pertinent de 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);

Ainsi, l'invocation new Random()sans aucun paramètre de départ prend le "seed uniquifier" actuel et le XOR avec System.nanoTime(). Ensuite, il utilise 181783497276652981pour créer un autre uniquificateur de départ à stocker pour la prochaine fois qu'il new Random()est appelé.

Les littéraux 181783497276652981Let 8682522807148012Lne sont pas placés dans des constantes, mais ils n'apparaissent nulle part ailleurs.

Au début, le commentaire me donne une piste facile. La recherche en ligne de cet article produit l'article réel . 8682522807148012n'apparaît pas dans le papier, mais 181783497276652981apparaît - comme sous-chaîne d'un autre nombre 1181783497276652981, qui est précédé d' 181783497276652981un 1.

Le papier prétend que 1181783497276652981c'est un nombre qui donne un bon «mérite» pour un générateur congruentiel linéaire. Ce numéro a-t-il été simplement mal copié dans Java? At 181783497276652981-il un mérite acceptable?

Et pourquoi a-t-il été 8682522807148012choisi?

La recherche en ligne de l'un ou l'autre des nombres ne donne aucune explication, seulement cette page qui remarque également la chute 1devant 181783497276652981.

Aurait-on pu choisir d'autres nombres qui auraient fonctionné aussi bien que ces deux nombres? Pourquoi ou pourquoi pas?

rgettman
la source
Je voudrais juste souligner qu'aucune des constantes mentionnées (même les plus grandes avec celles du début) n'est trop grande pour s'adapter bien que la multiplication entraînera sûrement un débordement.
nanofarad
6
8682522807148012est un héritage de la version précédente de la classe, comme on peut le voir dans les révisions effectuées en 2010 . Cela 181783497276652981Lsemble être une faute de frappe et vous pouvez déposer un rapport de bogue.
assylias
6
Soit c'est une faute de frappe, c'est-à-dire un bug, soit une fonctionnalité avec une motivation non révélée. Il faudrait demander aux auteurs. Tout ce que vous obtiendrez ici sera simplement une opinion plus ou moins non informée. Si vous pensez que c'est un bogue, soumettez un rapport de bogue.
Marquis of Lorne
1
Surtout compte tenu des différentes réponses, cela pourrait être deux questions distinctes pour chaque constante.
Mark Hurd
1
Triste de voir un goulot d'étranglement d'évolutivité mondiale intégré dans une classe aussi fondamentale. seedUniquifierpeut devenir extrêmement contesté sur une boîte de 64 noyaux. Un thread local aurait été plus évolutif.
usr

Réponses:

57
  1. Ce numéro a-t-il été simplement mal copié dans Java?

    Oui, semble être une faute de frappe.

  2. 181783497276652981 a-t-il un mérite acceptable?

    Cela pourrait être déterminé à l'aide de l'algorithme d'évaluation présenté dans l'article. Mais le mérite du nombre «original» est probablement plus élevé.

  3. Et pourquoi 8682522807148012 a-t-il été choisi?

    Semble être aléatoire. Cela pourrait être le résultat de System.nanoTime () lorsque le code a été écrit.

  4. Aurait-on pu choisir d'autres nombres qui auraient fonctionné aussi bien que ces deux nombres?

    Tous les nombres ne seraient pas également «bons». Donc non.

Stratégies d'ensemencement

Il existe des différences dans le schéma d'amorçage par défaut entre les différentes versions et l'implémentation du JRE.

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

Le premier n'est pas acceptable si vous créez plusieurs RNG à la suite. Si leurs temps de création tombent dans la même plage de millisecondes, ils donneront des séquences complètement identiques. (même graine => même séquence)

Le second n'est pas thread-safe. Plusieurs threads peuvent obtenir des RNG identiques lors de l'initialisation en même temps. De plus, les germes des initialisations ultérieures ont tendance à être corrélés. En fonction de la résolution de la minuterie réelle du système, la séquence d'amorçage pourrait être linéairement croissante (n, n + 1, n + 2, ...). Comme indiqué dans Dans quelle mesure les graines aléatoires doivent-elles être différentes? et l'article référencé Défauts communs dans l'initialisation des générateurs de nombres pseudo-aléatoires , les germes corrélés peuvent générer une corrélation entre les séquences réelles de plusieurs RNG.

La troisième approche crée des germes distribués de manière aléatoire et donc non corrélés, même entre les threads et les initialisations ultérieures. Donc, la documentation java actuelle:

Ce constructeur définit la valeur de départ du générateur de nombres aléatoires sur une valeur très probablement distincte de toute autre invocation de ce constructeur.

pourrait être étendu par "à travers les threads" et "non corrélé"

Qualité de la séquence de semences

Mais le caractère aléatoire de la séquence d'ensemencement n'est aussi bon que le RNG sous-jacent. Le RNG utilisé pour la séquence de départ dans cette implémentation java utilise un générateur congruentiel linéaire multiplicatif (MLCG) avec c = 0 et m = 2 ^ 64. (Le module 2 ^ 64 est implicitement donné par le débordement d'entiers longs de 64 bits) En raison du zéro c et du module de puissance de 2, la "qualité" (longueur de cycle, corrélation de bits, ...) est limitée . Comme le dit l'article, en plus de la longueur totale du cycle, chaque bit a sa propre longueur de cycle, qui diminue de manière exponentielle pour les bits moins significatifs. Ainsi, les bits inférieurs ont un motif de répétition plus petit. (Le résultat de seedUniquifier () doit être inversé en bits, avant d'être tronqué à 48 bits dans le RNG réel)

Mais c'est rapide! Et pour éviter les boucles de comparaison et de définition inutiles, le corps de la boucle doit être rapide. Ceci explique probablement l'utilisation de ce MLCG spécifique, sans ajout, sans xoring, juste une multiplication.

Et l'article mentionné présente une liste de bons "multiplicateurs" pour c = 0 et m = 2 ^ 64, comme 1181783497276652981.

Dans l'ensemble: A pour l'effort @ JRE-développeurs;) Mais il y a une faute de frappe. (Mais qui sait, à moins que quelqu'un ne l'évalue, il est possible que le premier 1 manquant améliore réellement le RNG de départ.)

Mais certains multiplicateurs sont certainement pires: "1" conduit à une séquence constante. "2" conduit à une séquence de déplacement d'un seul bit (en quelque sorte corrélée) ...

La corrélation inter-séquence pour les RNG est en fait pertinente pour les simulations (Monte Carlo), où plusieurs séquences aléatoires sont instanciées et même parallélisées. Une bonne stratégie de semis est donc nécessaire pour obtenir des simulations «indépendantes». Par conséquent, la norme C ++ 11 introduit le concept de séquence de semences pour générer des semences non corrélées.

Thomas B.
la source
3
Au moins, c'est toujours étrange, s'ils avaient laissé tomber le moins significatif au lieu du plus significatif, alors chaque multiplication perd un peu jusqu'à ce que finalement (après 62 étapes) le seedUniquifierdevienne bloqué à zéro.
harold
9

Si vous considérez que l'équation utilisée pour le générateur de nombres aléatoires est:

LCGEquation

Où X (n + 1) est le nombre suivant, a est le multiplicateur, X (n) est le nombre courant, c est l'incrément et m est le module.

Si vous regardez plus loin Random, a, c et m sont définis dans l'en-tête de la 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'

et en regardant la méthode où protected int next(int bits)l'équation est implémentée

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

Cela implique que la méthode seedUniquifier()obtient effectivement X (n) ou dans le premier cas à l'initialisation X (0) qui est en fait 8682522807148012 * 181783497276652981, cette valeur est ensuite modifiée davantage par la valeur de System.nanoTime(). Cet algorithme est cohérent avec l'équation ci-dessus mais avec le X (0) = 8682522807148012, a = 181783497276652981, m = 2 ^ 64 et c = 0. Mais comme le mod m de est préformé par le long débordement, l'équation ci-dessus devient

eq2

En regardant le papier , la valeur de a = 1181783497276652981est pour m = 2 ^ 64, c = 0. Il semble donc être juste une faute de frappe et la valeur 8682522807148012de X (0) qui semble être un nombre apparemment choisi au hasard dans le code hérité pour Random. Comme vu ici. Mais le mérite de ces nombres choisis pourrait encore être valable mais comme mentionné par Thomas B. probablement pas aussi «bon» que celui dans le papier.

EDIT - Les pensées originales ci-dessous ont depuis été clarifiées et peuvent donc être ignorées, mais en les laissant pour référence

Cela m'amène aux conclusions:

  1. La référence à l'article n'est pas pour la valeur elle-même mais pour les méthodes utilisées pour obtenir les valeurs dues aux différentes valeurs de a, c et m

  2. Ce n'est qu'une simple coïncidence si la valeur est par ailleurs la même autre que le premier 1 et le commentaire est mal placé (on a encore du mal à le croire)

OU

Il y a eu un grave malentendu sur les tableaux dans le document et les développeurs viennent de choisir une valeur au hasard car au moment où elle est multipliée, quel était l'intérêt d'utiliser la valeur de la table en premier lieu, d'autant plus que vous pouvez simplement fournir votre propre valeur de départ, auquel cas ces valeurs ne sont même pas prises en compte

Donc, pour répondre à votre question

Aurait-on pu choisir d'autres nombres qui auraient fonctionné aussi bien que ces deux nombres? Pourquoi ou pourquoi pas?

Oui, n'importe quel nombre aurait pu être utilisé, en fait si vous spécifiez une valeur de départ lors de l'instanciation aléatoire, vous utilisez une autre valeur. Cette valeur n'a aucun effet sur les performances du générateur, cela est déterminé par les valeurs de a, c et m qui sont codées en dur dans la classe.

Diable de Java
la source
1
Pas vraiment - Il existe deux algorithmes: (i) 1 pour créer une nouvelle graine aléatoire à chaque fois que le constructeur est appelé. Cet algo utilise un simple X_n + 1 = X_n * a. En raison d'un long débordement, cela équivaut à X_n + 1 = X_n * un mod m. Avec a = 181783497276652981 et m = 2 ^ 64. (ii) Un autre algo, qui, à partir d'une graine donnée, produit une série de nombres aléatoires. Ce deuxième algo est celui que vous mentionnez et les documents expliquent que " Ceci est un générateur de nombres pseudo-aléatoires congruentiels linéaires, comme décrit par Knuth dans The Art of Computer Programming ".
assylias
1
@assylias Je vois votre point de vue, je suis tellement pris dans le code source de Randomet l'article cité que j'ai complètement dépassé la question originale, sera bientôt édité, merci.
Java Devil
3

Selon le lien que vous avez fourni, ils ont choisi ( après avoir ajouté le 1 manquant :) ) le meilleur rendement de 2 ^ 64 car longtemps ne peut pas avoir un nombre de 2 ^ 128

Jaffar Ramay
la source