Comment générer efficacement de grands entiers randoms uniformément dissortingbués dans bash?

Je me demandais quelle serait la meilleure façon d'get un bon caractère random dans bash, c'est-à-dire quelle serait une procédure pour get un entier positif random entre MIN et MAX tel que

  1. La plage peut être arbitrairement grande (ou du less, disons, jusqu'à 2 32 -1);
  2. Les valeurs sont uniformément réparties (c.-à-d., Pas de biais);
  3. C'est efficace.

Un moyen efficace d'get du caractère random dans bash est d'utiliser la variable $RANDOM . Cependant, ceci n'échantillonne qu'une valeur comprise entre 0 et 2 15 -1, qui peut ne pas être suffisamment grande pour toutes les utilisations. Les gens utilisent généralement un modulo pour l'get dans la gamme qu'ils veulent, par exemple,

 MIN=0 MAX=12345 rnd=$(( $RANDOM % ($MAX + 1 - $MIN) + $MIN )) 

Ceci, en plus, crée un biais à less que $MAX arrive à split 2 15 -1 = 32767. Par exemple, si $MIN est 0 et que $MAX est 9, les valeurs 0 à 7 sont légèrement plus probables que les valeurs 8 et 9, car $RANDOM ne sera jamais 32768 ou 32769. , si $MIN est 0 et que $MAX est 9999, les nombres 0 à 2767 ont une probabilité de 4/32767 , alors que les nombres 2768 à 9999 n'ont qu'une probabilité de 3/32767 .

Donc, alors que la méthode ci-dessus remplit la condition 3, elle ne remplit pas les conditions 1 et 2.

La meilleure méthode que j'ai trouvée jusqu'ici pour essayer de satisfaire aux conditions 1 et 2 était d'utiliser /dev/urandom comme suit:

 MIN=0 MAX=1234567890 while rnd=$(cat /dev/urandom | tr -dc 0-9 | fold -w${#MAX} | head -1 | sed 's/^0*//;') [ -z $rnd ] && rnd=0 (( $rnd < $MIN || $rnd > $MAX )) do : done 

Fondamentalement, il suffit de collecter le caractère random de /dev/urandom (on peut envisager d'utiliser /dev/random si un générateur de nombres pseudo-randoms cryptographiquement fort est désiré, et si vous avez beaucoup de time, sinon un générateur de nombres randoms matériels) caractère qui n'est pas un chiffre décimal, pliez la sortie à la longueur de $MAX et coupez les 0 premiers. Si nous obtenons seulement 0, alors $rnd est vide, alors dans ce cas, définissez rnd sur 0 . Vérifiez si le résultat est hors de notre scope et si c'est le cas, répétez. J'ai forcé le «corps» de la boucle while dans la garde ici pour forcer l'exécution du corps au less une fois, dans l'esprit d'émuler un do ... while loop, puisque rnd n'est pas défini pour commencer.

Je pense que j'ai rempli les conditions 1 et 2 ici, mais maintenant j'ai foiré la condition 3. C'est un peu lent. Prend une seconde ou deux (dixièmes de seconde quand j'ai de la chance). En fait, la boucle n'est même pas garantie de se terminer (bien que la probabilité de résiliation converge vers 1 au fur et à mesure que le time augmente).

Existe-t-il un moyen efficace d'get des entiers randoms non biaisés, dans une plage pré-spécifiée et potentiellement large, dans bash? (Je continuerai à enquêter quand le time le permettra, mais en attendant, je pensais que quelqu'un ici pourrait avoir une bonne idée!)

Tableau des réponses

  1. L'idée la plus basique (et donc portable) est de générer une string de bits random juste assez longtime. Il existe différentes façons de générer une string de bits random, en utilisant la variable $RANDOM embeddede de bash ou en utilisant od et /dev/urandom (ou /dev/random ). Si le nombre random est supérieur à $MAX , recommencez.

    • Solution bash complète pour des plages arbitraires utilisant $RANDOM ou /dev/urandom
    • L'idée générale
    • Obtenez des strings randoms en utilisant openssl ou od avec /dev/urandom . Embellir avec tr .
    • Récupère des bitssortingng randoms en utilisant od avec /dev/random . Embellissez avec awk .
  2. Alternativement, il est possible d'utiliser des outils externes.

    • La solution Perl
      • Pro: assez portable, simple, flexible
      • Contra: pas pour les très grands nombres au-dessus de 2 32 -1
    • La solution Python
      • Pro: simple, flexible, fonctionne même pour les grands nombres
      • Contra: less portable
    • La solution zsh
      • Pro: bon pour les gens qui utilisent zsh quand même
      • Contra: probablement encore less portable

Je vois une autre méthode intéressante d' ici .

 rand=$(openssl rand 4 | od -DAn) 

Celui- ci semble également être une bonne option. Il lit 4 octets du périphérique random et les formate en tant qu'entier non signé entre 0 et 2^32-1 .

 rand=$(od -N 4 -t uL -An /dev/urandom | tr -d " ") 

Merci à tous pour vos excellentes réponses. J'ai fini avec la solution suivante, que je voudrais partager.

Avant d'entrer dans plus de détails sur les pourquoi et les comment, voici le tl; dr : mon nouveau script shiny 🙂

 #!/usr/bin/env bash # # Generates a random integer in a given range # computes the ceiling of log2 # ie, for parameter x returns the lowest integer l such that 2**l >= x log2() { local x=$1 n=1 l=0 while (( x>n && n>0 )) do let n*=2 l++ done echo $l } # uses $RANDOM to generate an n-bit random bitssortingng uniformly at random # (if we assume $RANDOM is uniformly dissortingbuted) # takes the length n of the bitssortingng as parameter, n can be up to 60 bits get_n_rand_bits() { local n=$1 rnd=$RANDOM rnd_bitlen=15 while (( rnd_bitlen < n )) do rnd=$(( rnd<<15|$RANDOM )) let rnd_bitlen+=15 done echo $(( rnd>>(rnd_bitlen-n) )) } # alternative implementation of get_n_rand_bits: # uses /dev/urandom to generate an n-bit random bitssortingng uniformly at random # (if we assume /dev/urandom is uniformly dissortingbuted) # takes the length n of the bitssortingng as parameter, n can be up to 56 bits get_n_rand_bits_alt() { local n=$1 local nb_bytes=$(( (n+7)/8 )) local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ") echo $(( rnd>>(nb_bytes*8-n) )) } # for parameter max, generates an integer in the range {0..max} uniformly at random # max can be an arbitrary integer, needs not be a power of 2 rand() { local rnd max=$1 # get number of bits needed to represent $max local bitlen=$(log2 $((max+1))) while # could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM rnd=$(get_n_rand_bits $bitlen) (( rnd > max )) do : done echo $rnd } # MAIN SCRIPT # check number of parameters if (( $# != 1 && $# != 2 )) then cat <<EOF 1>&2 Usage: $(basename $0) [min] max Returns an integer dissortingbuted uniformly at random in the range {min..max} min defaults to 0 (max - min) can be up to 2**60-1 EOF exit 1 fi # If we have one parameter, set min to 0 and max to $1 # If we have two parameters, set min to $1 and max to $2 max=0 while (( $# > 0 )) do min=$max max=$1 shift done # ensure that min <= max if (( min > max )) then echo "$(basename $0): error: min is greater than max" 1>&2 exit 1 fi # need absolute value of diff since min (and also max) may be negative diff=$((max-min)) && diff=${diff#-} echo $(( $(rand $diff) + min )) 

Enregistrez-le dans ~/bin/rand et vous avez à votre disposition une fonction random douce dans bash qui peut échantillonner un entier dans une plage arbitraire donnée. La plage peut contenir des entiers négatifs et positifs et peut atteindre 2 60 -1 de longueur:

 $ rand Usage: rand [min] max Returns an integer dissortingbuted uniformly at random in the range {min..max} min defaults to 0 (max - min) can be up to 2**60-1 $ rand 1 10 9 $ rand -43543 -124 -15757 $ rand -3 3 1 $ for i in {0..9}; do rand $((2**60-1)); done 777148045699177620 456074454250332606 95080022501817128 993412753202315192 527158971491831964 336543936737015986 1034537273675883580 127413814010621078 758532158881427336 924637728863691573 

Toutes les idées des autres répondeurs étaient formidables. Les réponses de terdon , JF Sebastian et jimmij ont utilisé des outils externes pour effectuer la tâche de manière simple et efficace. Cependant, j'ai préféré une vraie solution bash pour une portabilité maximale, et peut-être un peu, simplement par amour pour bash;)

Les réponses de Ramesh et l0b0 ont été utilisées /dev/urandom ou /dev/random en combinaison avec od . C'est bien, leurs approches ont cependant l'inconvénient de ne pouvoir échantillonner que des entiers randoms compris entre 0 et 2 8n -1, car cette méthode échantillonne des octets, c'est-à-dire des bitssortingngs de longueur 8. Ce sont de gros sauts avec augmenter n.

Enfin, la réponse de Falco décrit l'idée générale de comment cela pourrait être fait pour des gammes arbitraires (pas seulement des puissances de deux). Fondamentalement, pour une gamme donnée {0..max} , nous pouvons déterminer quelle est la puissance suivante de deux, c'est-à-dire exactement combien de bits sont nécessaires pour représenter max comme une string de bits. Ensuite, nous pouvons échantillonner juste de nombreux bits et voir si ce bissortingng, en tant qu'entier, est supérieur à max . Si c'est le cas, répétez. Puisque nous échantillonnons autant de bits que nécessaire pour représenter max , chaque itération a une probabilité supérieure ou égale à 50% de réussite (50% dans le pire des cas, 100% dans le meilleur des cas). Donc, c'est très efficace.

Mon script est fondamentalement une implémentation concrète de la réponse de Falco, écrite en pure bash et très efficace puisqu'elle utilise les opérations embeddedes au bit de bash pour échantillonner des strings de bits de la longueur désirée. Il honore en outre une idée d' Eliah Kagan qui suggère d'utiliser la variable $RANDOM embeddede en concaténant des strings de bits résultant d'appels répétés de $RANDOM . J'ai implémenté les deux possibilités d'utiliser /dev/urandom et $RANDOM . Par défaut, le script ci-dessus utilise $RANDOM . (Et bien, si vous utilisez /dev/urandom nous avons besoin de od et tr , mais ceux-ci sont supportés par POSIX.)

Alors, comment ça marche?

Avant d'entrer dans ceci, deux observations:

  1. Il s'avère que bash ne peut pas gérer des entiers supérieurs à 2 63 -1. Voir par vous-même:

     $ echo $((2**63-1)) 9223372036854775807 $ echo $((2**63)) -9223372036854775808 

    Il semblerait que bash utilise en interne des entiers 64 bits signés pour stocker des entiers. Donc, à 2 63, il "s'enroule" et nous obtenons un entier négatif. Nous ne pouvons donc pas espérer avoir une scope supérieure à 2 63 -1 avec n'importe quelle fonction random que nous utilisons. Bash ne peut tout simplement pas le gérer.

  2. Chaque fois que nous voulons échantillonner une valeur arbitraire entre min et max avec éventuellement min != 0 , nous pouvons simplement échantillonner une valeur entre 0 et max-min place, puis append min au résultat final. Cela fonctionne même si min et éventuellement aussi max sont négatifs , mais nous devons faire attention à échantillonner une valeur entre 0 et la valeur absolue de max-min . Alors, nous pouvons nous concentrer sur comment échantillonner une valeur random entre 0 et un entier positif arbitraire max . Le rest est facile.

Étape 1: Déterminez combien de bits sont nécessaires pour représenter un entier (le logarithme)

Donc, pour une valeur max donnée, nous voulons savoir combien de bits sont nécessaires pour la représenter en tant que bitssortingng. C'est pour que plus tard nous puissions échantillonner randomment seulement autant de bits que nécessaire, ce qui rend le script si efficace.

Voyons voir. Puisque avec n bits, nous pouvons représenter jusqu'à la valeur 2 n -1, alors le nombre n de bits nécessaire pour représenter une valeur arbitraire x est plafond (log 2 (x + 1)). Donc, nous avons besoin d'une fonction pour calculer le plafond d'un logarithme à la base 2. C'est plutôt explicite:

 log2() { local x=$1 n=1 l=0 while (( x>n && n>0 )) do let n*=2 l++ done echo $l } 

Nous avons besoin de la condition n>0 donc si elle grandit trop, s'enroule et devient négative, la boucle se termine.

Étape 2: Échantillonner une string de bits random de longueur n

Les idées les plus portables sont soit d'utiliser /dev/urandom (ou même /dev/random s'il y a une raison forte), soit la variable $RANDOM embeddede de bash. Regardons comment le faire avec $RANDOM premier.

Option A: utiliser $RANDOM

Cela utilise l' idée mentionnée par Eliah Kagan. Fondamentalement, puisque $RANDOM échantillonne un entier de 15 bits, nous pouvons utiliser $((RANDOM<<15|RANDOM)) pour échantillonner un entier de 30 bits. Cela signifie, décaler une première invocation de $RANDOM de 15 bits vers la gauche, et appliquer un bitwise ou avec une deuxième invocation de $RANDOM , concaténant efficacement deux bitssortingngs échantillonnés indépendamment (ou au less aussi indépendants que $RANDOM embedded de bash va ).

Nous pouvons répéter ceci pour get un entier de 45 bits ou de 60 bits. Après cela, bash ne peut plus le gérer, mais cela signifie que nous pouvons facilement échantillonner une valeur random entre 0 et 2 60 -1. Donc, pour échantillonner un entier à n bits, nous répétons la procédure jusqu'à ce que notre string de bits random, dont la longueur augmente par pas de 15 bits, ait une longueur supérieure ou égale à n. Finalement, nous coupons les bits qui sont trop forts en décalant de manière appropriée le bit vers la droite, et nous nous retrouvons avec un entier random de n bits.

 get_n_rand_bits() { local n=$1 rnd=$RANDOM rnd_bitlen=15 while (( rnd_bitlen < n )) do rnd=$(( rnd<<15|$RANDOM )) let rnd_bitlen+=15 done echo $(( rnd>>(rnd_bitlen-n) )) } 

Option B: Utiliser /dev/urandom

Alternativement, nous pouvons utiliser od et /dev/urandom pour échantillonner un entier à n bits. od va lire les octets, c'est-à-dire les strings de bits de longueur 8. De même que dans la méthode précédente, nous échantillonnons juste autant d'octets que le nombre équivalent de bits échantillonnés est supérieur ou égal à n et coupe les bits qui sont trop.

Le plus petit nombre d'octets requirejs pour get au less n bits est le plus petit multiple de 8 supérieur ou égal à n, c'est-à-dire floor ((n + 7) / 8).

Cela ne fonctionne que jusqu'à des entiers 56 bits. Échantillonner un octet de plus nous obtiendrait un entier de 64 bits, c'est-à-dire une valeur allant jusqu'à 2 64 -1, que bash ne peut pas gérer.

 get_n_rand_bits_alt() { local n=$1 local nb_bytes=$(( (n+7)/8 )) local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ") echo $(( rnd>>(nb_bytes*8-n) )) } 

Mettre les pièces set: get des entiers randoms dans des plages arbitraires

Nous pouvons maintenant échantillonner des bitssortingngs à n bits, mais nous voulons échantillonner des entiers dans une plage de 0 à max , uniformément au hasard , où max peut être arbitraire, pas nécessairement une puissance de deux. (Nous ne pouvons pas utiliser modulo car cela crée un biais.)

Le point entier pour lequel nous avons essayé si fort d'échantillonner autant de bits que nécessaire pour représenter la valeur max est que nous pouvons maintenant utiliser une boucle de façon sécuritaire et efficace pour échantillonner de manière répétée une string de bits n bits jusqu'à ce que nous échantillonnions une valeur qui est inférieur ou égal à max . Dans le pire des cas ( max est une puissance de deux), chaque itération se termine avec une probabilité de 50%, et dans le meilleur des cas ( max est une puissance de deux less un), la première itération se termine avec certitude.

 rand() { local rnd max=$1 # get number of bits needed to represent $max local bitlen=$(log2 $((max+1))) while # could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM rnd=$(get_n_rand_bits $bitlen) (( rnd > max )) do : done echo $rnd } 

Envelopper les choses

Enfin, nous voulons échantillonner des entiers entre min et max , où min et max peuvent être arbitraires, voire négatifs. Comme mentionné précédemment, c'est maintenant sortingvial.

Mettons tout dans un script bash. Faites un argument en analysant des trucs … Nous voulons deux arguments min et max , ou seulement un argument max , où min vaut par défaut 0 .

 # check number of parameters if (( $# != 1 && $# != 2 )) then cat <<EOF 1>&2 Usage: $(basename $0) [min] max Returns an integer dissortingbuted uniformly at random in the range {min..max} min defaults to 0 (max - min) can be up to 2**60-1 EOF exit 1 fi # If we have one parameter, set min to 0 and max to $1 # If we have two parameters, set min to $1 and max to $2 max=0 while (( $# > 0 )) do min=$max max=$1 shift done # ensure that min <= max if (( min > max )) then echo "$(basename $0): error: min is greater than max" 1>&2 exit 1 fi 

… et enfin d'échantillonner uniformément au hasard une valeur entre min et max , nous échantillonnons un entier random compris entre 0 et la valeur absolue de max-min et ajoutons min au résultat final. 🙂

 diff=$((max-min)) && diff=${diff#-} echo $(( $(rand $diff) + min )) 

Inspiré par cela , je pourrais essayer d'utiliser dieharder pour tester et comparer ce PRNG, et mettre mes conclusions ici. 🙂

Peut-il être zsh?

 max=1000 integer rnd=$(( $(( rand48() )) * $max )) 

Vous pouvez également utiliser des graines avec rand48(seed) . Voir l' man zshmodules et man 3 erand48 pour une description détaillée si vous man 3 erand48 intéressé.

 $ python -c 'import random as R; print(R.randint(-3, 5**1234))' 

python est disponible sur Redhat, les systèmes basés sur Debian.

Si vous voulez un nombre de 0 à (2 ^ n) -1n mod 8 = 0, vous pouvez simplement get n / 8 octets de /dev/random . Par exemple, pour get la représentation décimale d'un int random, vous pouvez:

 od --read-bytes=4 --address-radix=n --format=u4 /dev/random | awk '{print $1}' 

Si vous voulez prendre seulement n bits, vous pouvez d'abord prendre le plafond (n / 8) octets et déplacer à droite le montant que vous voulez. Par exemple si vous voulez 15 bits:

 echo $(($(od --read-bytes=2 --address-radix=n --format=u4 /dev/random | awk '{print $1}') >> 1)) 

Si vous êtes absolument sûr que vous ne vous souciez pas de la qualité du caractère random et que vous voulez garantir un time d'exécution minimal, vous pouvez utiliser /dev/urandom au lieu de /dev/random . Assurez-vous de savoir ce que vous faites avant d'utiliser /dev/urandom !

En supposant que vous ne vous opposez pas à l'utilisation d'outils externes, cela devrait répondre à vos exigences:

 rand=$(perl -e 'print int(rand(2**32-1))'); 

Il utilise la fonction rand de perl qui prend une limite supérieure en tant que paramètre. Vous pouvez le définir comme vous voulez. La proximité de cette définition mathématique abstraite dépasse la scope de ce site, mais elle devrait être correcte à less d'en avoir besoin pour un encryption extrêmement sensible ou similaire. Peut-être même là mais je ne vais pas oser une opinion.

Vous devriez get le plus proche (2 ^ X) -1 égal ou plus grand que votre maximum désiré et get le nombre de bits. Ensuite, appelez / dev / random plusieurs fois et ajoutez tous les bits set jusqu'à ce que vous en ayez assez, en tronquant tous les bits qui sont trop. Si le nombre résultant est plus grand que votre répétition max. Dans le pire des cas, vous avez plus de 50% de chances d'get un nombre random inférieur à votre maximum, alors (pour ce pire cas), vous prendrez deux appels en moyenne.

Votre réponse est intéressante mais assez longue.

Si vous voulez arbitrairement de grands nombres, alors vous pouvez joindre plusieurs nombres randoms dans un assistant:

 # $1 - number of 'digits' of size base function random_helper() { base=32768 random=0 for((i=0; i<$1; ++i)); do let "random+=$RANDOM*($base**$i)" done echo $random } 

Si le problème est biaisé, il suffit de le supprimer.

 # $1 - min value wanted # $2 - max value wanted function random() { MAX=32767 min=$1 max=$(($2+1)) size=$((max-min)) bias_range=$((MAX/size)) while random=$RANDOM [ $((random/size)) -eq $bias_range ]; do :; done echo $((random%size+min)) } 

Rejoindre ces fonctions set

 # $1 - min value wanted # $2 - max value wanted # $3 - number of 'digits' of size base function random() { base=32768 MAX=$((base**$3-1)) min=$1 max=$(($2+1)) size=$((max-min)) bias_range=$((MAX/size)) while random=$(random_helper) [ $((random/size)) -eq $bias_range ]; do :; done echo $((random%size+min)) }