web-dev-qa-db-fra.com

Quel est le moyen le plus rapide de générer un fichier texte de 1 Go contenant des chiffres aléatoires?

J'ai essayé un script bash, mais il a fallu trop de temps pour créer un simple fichier de 1 Mo. Je pense que la réponse réside dans l'utilisation de /dev/random ou /dev/urandom, mais d'autres articles ici montrent uniquement comment ajouter toutes sortes de données à un fichier en utilisant ces éléments, mais je veux ajouter uniquement des chiffres.

Alors, existe-t-il une commande que je peux utiliser pour créer un fichier aléatoire de taille 1 Go contenant uniquement des nombres entre 0 et 9?

Edit: je veux que la sortie soit quelque chose comme ça

0 1 4 7 ..... 9
8 7 5 8 ..... 8
....
....
8 7 5 3 ..... 3

La plage est comprise entre 0 et 9, ce qui signifie que les chiffres 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9. J'ai également besoin qu'ils soient séparés par des espaces et 100 par ligne, jusqu'à n nombre de lignes. Ce n est quelque chose que je ne me soucie pas, je veux que ma taille finale soit de 1 Go.

Edit: j'utilise Ubuntu 16.04 LTS

53
posixKing

Il s'agit en partie d'une réponse ironique, en raison du titre de la question.

Lorsque vous recherchez "le moyen le plus rapide de ..." , la réponse est presque toujours un outil spécialisé. Cette "réponse" montre un tel outil, juste pour que vous puissiez expérimenter.

Ce n'est pas une réponse sérieuse, car vous ne devriez pas chercher des outils spécialisés pour des emplois que vous ne faites qu'une seule fois, ou très rarement. Vous voyez, vous finirez par passer plus de temps à chercher des outils et à en apprendre davantage à leur sujet qu'à faire des trucs. Les shells et les utilitaires comme bash et awk ne sont pas les plus rapides, mais vous pouvez généralement écrire une ligne dans réaliser le travail, en ne passant que quelques secondes. De meilleurs langages de script comme Perl peuvent également être utilisés, bien que la courbe d'apprentissage de Perl soit abrupte, et j'hésite à le recommander à de telles fins, car j'ai été traumatisé par d'horribles projets Perl. python d'autre part est légèrement handicapé par ses E/S plutôt lentes; cependant, ce n'est un problème que lorsque vous filtrez ou générez des gigaoctets de données.

Dans tous les cas, l'exemple de programme C89 suivant (qui utilise POSIX.1 pour une horloge de précision supérieure uniquement si disponible) devrait atteindre un taux de génération d'environ 100 Mo/s (testé sous Linux sur un ordinateur portable avec un processeur Intel i5-4200U, canalisant la sortie à /dev/null), en utilisant un assez bon générateur de nombres pseudo-aléatoires. (La sortie doit réussir tous les tests BigCrunch, à l'exception du test MatrixRank, car le code utilise xorshift64 * et la méthode d'exclusion pour éviter de biaiser les chiffres.)

decimal-digits.c:

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <locale.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>

/* This program is licensed under the CC0 license,
       https://creativecommons.org/publicdomain/zero/1.0/
   In other words, this is dedicated to the public domain.
   There are no warranties either, so if something breaks,
   you only have yourself to blame.
*/

#if _POSIX_C_SOURCE-199309 >= 0
static uint64_t time_seed(void)
{
    struct timespec  ts;

    if (clock_gettime(CLOCK_REALTIME, &ts))
        return (uint64_t)time(NULL);

    return (uint64_t)ts.tv_sec
         ^ (((uint64_t)ts.tv_nsec) << 32);
}
#else
static uint64_t time_seed(void)
{
    return (uint64_t)time(NULL);
}
#endif

/* Preferred output I/O block size.
 * Currently, about 128k blocks yield
 * maximum I/O throughput on most devices.
 * Note that this is a heuristic value,
 * and may be increased in the future.
*/
#ifndef  IO_BLOCK_SIZE
#define  IO_BLOCK_SIZE  262144
#endif

/* This is the Xorshift* pseudo-random number generator.
 * See https://en.wikipedia.org/wiki/Xorshift#xorshift.2A
 * for details. This is an incredibly fast generator that
 * passes all but the MatrixRank test of the BigCrush
 * randomness test suite, with a period of 2^64-1.
 * Note that neither xorshift_state, nor the result of
 * this function, will ever be zero.
*/
static uint64_t xorshift_state;

static uint64_t xorshift_u64(void)
{
    xorshift_state ^= xorshift_state >> 12;
    xorshift_state ^= xorshift_state << 25;
    xorshift_state ^= xorshift_state >> 27;
    return xorshift_state * UINT64_C(2685821657736338717);
}

/* This function returns a number between (inclusive)
 * 0 and 999,999,999,999,999,999 using xorshift_u64()
 * above, using the exclusion method. Thus, there is
 * no bias in the results, and each digit should be
 * uniformly distributed in 0-9.
*/
static uint64_t quintillion(void)
{
    uint64_t result;

    do {
        result = xorshift_u64() & UINT64_C(1152921504606846975);
    } while (!result || result > UINT64_C(1000000000000000000));

    return result - UINT64_C(1);
}

/* This function returns a single uniformly random digit.
*/
static unsigned char digit(void)
{
    static uint64_t       digits_cache = 0;
    static unsigned char  digits_cached = 0;
    unsigned char         retval;

    if (!digits_cached) {
        digits_cache = quintillion();
        digits_cached = 17; /* We steal the first one! */
    } else
        digits_cached--;

    retval = digits_cache % (uint64_t)(10);
    digits_cache /= (uint64_t)(10);

    return retval;
}

static int parse_ulong(const char *src, unsigned long *to)
{
    const char   *end = src;
    unsigned long value;

    if (!src)
        return errno = EINVAL;

    errno = 0;
    value = strtoul(src, (char **)&end, 0);
    if (errno)
        return errno;

    if (end == src)
        return errno = EINVAL;
    while (*end)
        if (isspace(*end))
            end++;
        else
            return errno = EINVAL;

    if (to)
        *to = value;
    return 0;
}

int main(int argc, char *argv[])
{
    unsigned long lines, cols, line, col, seed;

    /* When parsing the command-line parameters,
     * use locale conventions. */
    setlocale(LC_ALL, "");

    /* Standard output should be fully buffered, if possible.
     * This only affects output speed, so we're not too worried
     * if this happens to fail. */
    (void)setvbuf(stdout, NULL, _IOFBF, (size_t)IO_BLOCK_SIZE);

    if (argc < 3 || argc > 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s COLS LINES [ SEED ]\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program generates random decimal digits\n");
        fprintf(stderr, "0 - 9, separated by spaces, COLS per line,\n");
        fprintf(stderr, "LINES lines.  In total, COLS*LINES*2 bytes\n");
        fprintf(stderr, "will be used.\n");
        fprintf(stderr, "\n");
        fprintf(stderr, "SEED is the optional seed for the Xorshift64*\n");
        fprintf(stderr, "pseudo-random number generator used in this program.\n");
        fprintf(stderr, "If omitted, current time is used as the seed.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    if (parse_ulong(argv[1], &cols) || cols < 1UL) {
        fprintf(stderr, "%s: Invalid number of digits per line.\n", argv[1]);
        return EXIT_FAILURE;
    }
    if (parse_ulong(argv[2], &lines) || lines < 1UL) {
        fprintf(stderr, "%s: Invalid number of lines.\n", argv[2]);
        return EXIT_FAILURE;
    }

    if (argc > 3) {
        if (parse_ulong(argv[3], &seed)) {
            fprintf(stderr, "%s: Invalid Xorshift64* seed.\n", argv[3]);
            return EXIT_FAILURE;
        }
    } else
        seed = time_seed();

    /* Since zero seed is invalid, we map it to ~0. */
    xorshift_state = seed;
    if (!xorshift_state)
        xorshift_state = ~(uint64_t)0;

    /* Discard first 1000 values to make the initial values unpredictable. */
    for (col = 0; col < 1000; col++)
        xorshift_u64();

    for (line = 0UL; line < lines; line++) {
        fputc('0' + digit(), stdout);
        for (col = 1UL; col < cols; col++) {
            fputc(' ', stdout);
            fputc('0' + digit(), stdout);
        }
        fputc('\n', stdout);

        /* Check for write errors. */
        if (ferror(stdout))
            return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Nous pouvons le rendre beaucoup plus rapide, si nous passons à un tampon de ligne, et fwrite() une fois au lieu de sortir chaque chiffre à la fois. Notez que nous gardons toujours le flux entièrement tamponné, pour éviter les écritures partielles (sans puissance de deux) si la sortie est un périphérique de bloc.

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <locale.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>

#if _POSIX_C_SOURCE-199309 >= 0
static uint64_t time_seed(void)
{
    struct timespec  ts;

    if (clock_gettime(CLOCK_REALTIME, &ts))
        return (uint64_t)time(NULL);

    return (uint64_t)ts.tv_sec
         ^ (((uint64_t)ts.tv_nsec) << 32);
}
#else
static uint64_t time_seed(void)
{
    return (uint64_t)time(NULL);
}
#endif

/* Preferred output I/O block size.
 * Currently, about 128k blocks yield
 * maximum I/O throughput on most devices.
 * Note that this is a heuristic value,
 * and may be increased in the future.
*/
#ifndef  IO_BLOCK_SIZE
#define  IO_BLOCK_SIZE  262144
#endif

/* This is the Xorshift* pseudo-random number generator.
 * See https://en.wikipedia.org/wiki/Xorshift#xorshift.2A
 * for details. This is an incredibly fast generator that
 * passes all but the MatrixRank test of the BigCrush
 * randomness test suite, with a period of 2^64-1.
 * Note that neither xorshift_state, nor the result of
 * this function, will ever be zero.
*/
static uint64_t xorshift_state;

static uint64_t xorshift_u64(void)
{
    xorshift_state ^= xorshift_state >> 12;
    xorshift_state ^= xorshift_state << 25;
    xorshift_state ^= xorshift_state >> 27;
    return xorshift_state * UINT64_C(2685821657736338717);
}

/* This function returns a number between (inclusive)
 * 0 and 999,999,999,999,999,999 using xorshift_u64()
 * above, using the exclusion method. Thus, there is
 * no bias in the results, and each digit should be
 * uniformly distributed in 0-9.
*/
static uint64_t quintillion(void)
{
    uint64_t result;

    do {
        result = xorshift_u64() & UINT64_C(1152921504606846975);
    } while (!result || result > UINT64_C(1000000000000000000));

    return result - UINT64_C(1);
}

/* This function returns a single uniformly random digit.
*/
static unsigned char digit(void)
{
    static uint64_t       digits_cache = 0;
    static unsigned char  digits_cached = 0;
    unsigned char         retval;

    if (!digits_cached) {
        digits_cache = quintillion();
        digits_cached = 17; /* We steal the first one! */
    } else
        digits_cached--;

    retval = digits_cache % (uint64_t)(10);
    digits_cache /= (uint64_t)(10);

    return retval;
}

static int parse_ulong(const char *src, unsigned long *to)
{
    const char   *end = src;
    unsigned long value;

    if (!src)
        return errno = EINVAL;

    errno = 0;
    value = strtoul(src, (char **)&end, 0);
    if (errno)
        return errno;

    if (end == src)
        return errno = EINVAL;
    while (*end)
        if (isspace(*end))
            end++;
        else
            return errno = EINVAL;

    if (to)
        *to = value;
    return 0;
}

int main(int argc, char *argv[])
{
    unsigned long lines, cols, line, col, seed;
    char         *oneline;

    /* When parsing the command-line parameters,
     * use locale conventions. */
    setlocale(LC_ALL, "");

    /* Standard output should be fully buffered, if possible.
     * This only affects output speed, so we're not too worried
     * if this happens to fail. */
    (void)setvbuf(stdout, NULL, _IOFBF, (size_t)IO_BLOCK_SIZE);

    if (argc < 3 || argc > 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s COLS LINES [ SEED ]\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program generates random decimal digits\n");
        fprintf(stderr, "0 - 9, separated by spaces, COLS per line,\n");
        fprintf(stderr, "LINES lines.  In total, COLS*LINES*2 bytes\n");
        fprintf(stderr, "will be used.\n");
        fprintf(stderr, "\n");
        fprintf(stderr, "SEED is the optional seed for the Xorshift64*\n");
        fprintf(stderr, "pseudo-random number generator used in this program.\n");
        fprintf(stderr, "If omitted, current time is used as the seed.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    if (parse_ulong(argv[1], &cols) || cols < 1UL) {
        fprintf(stderr, "%s: Invalid number of digits per line.\n", argv[1]);
        return EXIT_FAILURE;
    }
    if (parse_ulong(argv[2], &lines) || lines < 1UL) {
        fprintf(stderr, "%s: Invalid number of lines.\n", argv[2]);
        return EXIT_FAILURE;
    }

    if (argc > 3) {
        if (parse_ulong(argv[3], &seed)) {
            fprintf(stderr, "%s: Invalid Xorshift64* seed.\n", argv[3]);
            return EXIT_FAILURE;
        }
    } else
        seed = time_seed();

    /* Since zero seed is invalid, we map it to ~0. */
    xorshift_state = seed;
    if (!xorshift_state)
        xorshift_state = ~(uint64_t)0;

    /* Discard first 1000 values to make the initial values unpredictable. */
    for (col = 0; col < 1000; col++)
        xorshift_u64();

    /* Allocate memory for a full line. */
    oneline = malloc((size_t)(2 * cols + 1));
    if (!oneline) {
        fprintf(stderr, "Not enough memory for %lu column buffer.\n", cols);
        return EXIT_FAILURE;
    }

    /* Set spaces and terminating newline. */
    for (col = 0; col < cols; col++)
        oneline[2*col + 1] = ' ';
    oneline[2*cols-1] = '\n';

    /* Not needed, but in case a code modification treats it as a string. */
    oneline[2*cols] = '\0';

    for (line = 0UL; line < lines; line++) {
        for (col = 0UL; col < cols; col++)
            oneline[2*col] = digit();

        if (fwrite(oneline, 2*cols, 1, stdout) != 1)
            return EXIT_FAILURE; 
    }

    /* Check for write errors. */
    if (ferror(stdout))
        return EXIT_FAILURE;

    return EXIT_SUCCESS;
}

Remarque: les deux exemples édités le 2016-11-18 pour assurer une distribution uniforme des chiffres (zéro est exclu; voir par exemple ici pour comparaison et détails sur divers générateurs de nombres pseudo-aléatoires).

Compiler en utilisant par exemple

gcc -Wall -O2 decimal-digits.c -o decimal-digits

et éventuellement installer à l'échelle du système sur /usr/bin en utilisant

Sudo install -o root -g root -m 0755 decimal-digits /usr/bin

Il prend le nombre de chiffres par ligne et le nombre de lignes. Étant donné que 1000000000 / 100 / 2 = 5000000 (Cinq millions; octets totaux divisés par des colonnes divisées par 2), vous pouvez utiliser

./decimal-digits 100 5000000 > digits.txt

pour générer le gigaoctet de taille digits.txt comme souhaité par l'OP.

Notez que le programme lui-même est écrit avec plus de lisibilité que d'efficacité à l'esprit. Mon intention ici n'est pas de montrer l'efficacité du code - j'utiliserais de toute façon POSIX.1 et des E/S de bas niveau, plutôt que des interfaces C génériques - mais de vous permettre de voir facilement quel genre d'équilibre il y a avec l'effort dépensé dans le développement d'outils dédiés par rapport à leurs performances, par rapport aux lignes simples ou aux scripts courts Shell ou awk.

En utilisant la bibliothèque GNU C, appeler la fonction fputc() pour chaque sortie de caractère entraîne une très petite surcharge (d'un appel de fonction indirecte ou conditionnelle - la FILE l'interface est en fait assez complexe et polyvalente, vous voyez). Sur cet ordinateur portable Intel Core i5-4200U particulier, la redirection de la sortie vers /dev/null, la première version (fputc) prend environ 11 secondes, tandis que la ligne- la version à la fois ne prend que 1,3 seconde.

Il m'arrive d'écrire souvent de tels programmes et générateurs uniquement parce que j'aime jouer avec d'énormes ensembles de données. Je suis bizarre de cette façon. Par exemple, j'ai écrit un programme pour imprimer toutes les valeurs à virgule flottante IEEE-754 positives finies dans un fichier texte, avec une précision suffisante pour produire exactement la même valeur lors de l'analyse. Le fichier avait une taille de quelques gigaoctets (peut-être 4G ou plus); il n'y a pas autant de float positifs finis qu'on pourrait le penser. J'ai utilisé cela pour comparer les implémentations qui lisent et analysent ces données.

Pour les cas d'utilisation normaux, comme l'OP, les scripts et les scriptlets Shell et les monolignes sont la meilleure approche. Moins de temps passé pour accomplir la tâche globale. (Sauf s'ils ont besoin d'un fichier différent tous les jours ou si beaucoup de gens ont besoin d'un fichier différent, dans lequel - rare - un outil dédié comme ci-dessus, pourrait justifier l'effort dépensé.)

38
Nominal Animal

Cette:

 LC_ALL=C tr '\0-\377' \
             '[0*25][1*25][2*25][3*25][4*25][5*25][6*25][7*25][8*25][9*25][x*]' \
    < /dev/urandom |
    tr -d x |
    fold -w 1 |
    paste -sd "$(printf '%99s\\n')" - |
    head -c1G

(en supposant une implémentation head qui prend en charge -c) semble être assez rapide sur mon système.

tr traduit toute la plage d'octets (0 à 255, 0 à 0377 en octal): les 25 premiers octets à 0, les 25 suivants à 1 ... puis 25 9 le reste (250 à 255) en "x" que nous éliminons ensuite (avec tr -d x) car nous voulons une distribution uniforme (en supposant que /dev/urandom a lui-même une distribution uniforme) et donc ne donnons pas de biais à certains chiffres.

Cela produit un chiffre pour 97% des octets de /dev/urandom. fold -w 1 Fait un chiffre par ligne. paste -s Est appelé avec une liste de séparateurs qui se compose de 99 caractères d'espace et d'un caractère de nouvelle ligne, afin d'avoir 100 chiffres séparés par des espaces sur chaque ligne.

head -c1G Obtiendra le premier GiB (230) de ça. Notez que la dernière ligne sera tronquée et non délimitée. Vous pourriez tronquer à 230-1 et ajoutez manuellement la nouvelle ligne manquante ou tronquez-la à 109 octets à la place, ce qui représente 50 millions de ces lignes de 200 octets (head -n 50000000 en ferait également une commande standard/portable).

Ces timings (obtenus par zsh sur un système quad-core), donnent une indication de l'endroit où le temps CPU est dépensé:

LC_ALL=C tr '\0-\377'  < /dev/urandom  0.61s user 31.28s system 99% cpu 31.904 total
tr -d x  1.00s user 0.27s system 3% cpu 31.903 total
fold -w 1  14.93s user 0.48s system 48% cpu 31.902 total
paste -sd "$(printf '%99s\\n')" -  7.23s user 0.08s system 22% cpu 31.899 total
head -c1G > /dev/null  0.49s user 1.21s system 5% cpu 31.898 total

Le premier tr est le goulot d'étranglement, la plupart du temps passé dans le noyau (je suppose pour la génération de nombres aléatoires). Le timing est à peu près en ligne avec le taux que je peux obtenir des octets de /dev/uramdom (Environ 19 Mo/s et ici nous produisons 2 octets pour chaque 0,97 octet de/dev/urandom à un taux de 32 Mo/s). fold semble passer un temps déraisonnable (15s) juste pour insérer un caractère de nouvelle ligne après chaque octet, mais cela n'affecte pas le temps global car il fonctionne sur un autre processeur dans mon cas (en ajoutant le L'option -b La rend très légèrement plus efficace, dd cbs=1 conv=unblock Semble être une meilleure alternative).

Vous pouvez supprimer le head -c1G Et vous raser quelques secondes en fixant une limite à la taille du fichier (limit filesize 1024m Avec zsh ou ulimit -f "$((1024*1024))" avec la plupart d'autres shells (y compris zsh)) à la place dans un sous-shell.

Cela pourrait être amélioré si nous extrayions 2 chiffres pour chaque octet, mais nous aurions besoin d'une approche différente pour cela. Ce qui précède est très efficace car tr recherche simplement chaque octet dans un tableau de 256 octets. Il ne peut pas faire cela pendant 2 octets à la fois, et utiliser des choses comme hexdump -e '1/1 "%02u"' Qui calcule la représentation textuelle d'un octet en utilisant des algorithmes plus complexes serait plus coûteuse que la génération de nombres aléatoires elle-même. Pourtant, si comme dans mon cas, vous avez des cœurs de processeur dont le temps à perdre, il peut tout de même réussir à se raser quelques secondes:

Avec:

< /dev/urandom LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' |
  tr -d x |
  hexdump -n250000000 -ve '500/1 "%02u" "\n"' |
  fold -w1 |
  paste -sd "$(printf '%99s\\n')" - > /dev/null

Je reçois (notez cependant qu'ici c'est 10000000000 d'octets au lieu de 10731741 824):

LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' < /dev/urandom  0.32s user 18.83s system 70% cpu 27.001 total
tr -d x  2.17s user 0.09s system 8% cpu 27.000 total
hexdump -n250000000 -ve '500/1 "%02u" "\n"'  26.79s user 0.17s system 99% cpu 27.000 total
fold -w1  14.42s user 0.67s system 55% cpu 27.000 total
paste -sd "$(printf '%99s\\n')" - > /dev/null  8.00s user 0.23s system 30% cpu 26.998 total

Plus de temps CPU dans l'ensemble, mais mieux réparti entre mes 4 cœurs CPU, donc cela finit par prendre moins de temps d'horloge murale. Le goulot d'étranglement est désormais hexdump.

Si nous utilisons dd au lieu de la ligne fold, nous pouvons réellement réduire la quantité de travail hexdump à faire et améliorer l'équilibre du travail entre les CPU:

< /dev/urandom LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' |
  tr -d x |
  hexdump -ve '"%02u"' |
  dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock |
  paste -sd "$(printf '%99s\\n')" -

(ici en supposant GNU dd pour ses iflag=fullblock et status=none) qui donne:

LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' < /dev/urandom  0.32s user 15.58s system 99% cpu 15.915 total
tr -d x  1.62s user 0.16s system 11% cpu 15.914 total
hexdump -ve '"%02u"'  10.90s user 0.32s system 70% cpu 15.911 total
dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock  5.44s user 0.19s system 35% cpu 15.909 total
paste -sd "$(printf '%99s\\n')" - > /dev/null  5.50s user 0.30s system 36% cpu 15.905 total

Revenons à la génération de nombres aléatoires qui est le goulot d'étranglement.

Maintenant, comme l'a souligné @OleTange, si vous avez l'utilitaire openssl, vous pouvez l'utiliser pour obtenir un générateur d'octets pseudo-aléatoire plus rapide (en particulier sur les processeurs qui ont des instructions AES).

</dev/zero openssl enc -aes-128-ctr -nosalt -pass file:/dev/urandom

sur mon système crache 15 fois plus d'octets par seconde que /dev/urandom. (Je ne peux pas commenter la comparaison en termes de source d'aléatoire cryptographiquement sécurisée si cela s'applique à votre cas d'utilisation).

</dev/zero openssl enc -aes-128-ctr -nosalt -pass file:/dev/urandom 2> /dev/null | 
  LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' |
  tr -d x |
  hexdump -ve '"%02u"' |
  dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock |
  paste -sd "$(printf '%99s\\n')" -

Donne maintenant:

openssl enc -aes-128-ctr -nosalt -pass file:/dev/urandom < /dev/zero 2>   1.13s user 0.16s system 12% cpu 10.174 total
LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]'  0.56s user 0.20s system 7% cpu 10.173 total
tr -d x  2.50s user 0.10s system 25% cpu 10.172 total
hexdump -ve '"%02u"'  9.96s user 0.19s system 99% cpu 10.172 total
dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock  4.38s user 0.20s system 45% cpu 10.171 total
paste -sd "$(printf '%99s\\n')" - > /dev/null

retour à hexdump étant le goulot d'étranglement.

Comme j'ai encore des CPU à revendre, je peux exécuter 3 de ces hexdump en parallèle.

</dev/zero openssl enc -aes-128-ctr -nosalt -pass file:/dev/urandom 2> /dev/null | 
  LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' |
  tr -d x |
  (hexdump -ve '"%02u"' <&3 & hexdump -ve '"%02u"' <&3 & hexdump -ve '"%02u"') 3<&0 |
  dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock |
  paste -sd "$(printf '%99s\\n')" -

(le <&3 est nécessaire pour les shells autres que zsh qui ferment les commandes stdin sur/dev/null lorsqu'elles sont exécutées en arrière-plan).

Maintenant à 6,2 secondes et mes processeurs presque entièrement utilisés.

81
Stéphane Chazelas

Si vous avez shuf disponible (récent GNU coreutils le fait) vous pouvez le faire:

time shuf -r -n $((512*1024*1024)) -i 0-9 | paste -sd "$(printf '%99s\\n')" -

Sur ma machine virtuelle, cela est maintenant un peu plus lent que la réponse de Stéphane d'environ 3: 4.

23
Digital Trauma

Si vous n'avez pas besoin d'aléatoire de très haute qualité et que la distribution proche de l'uniforme est assez bonne, vous pouvez aller vraiment rapidement, en particulier sur un processeur moderne avec des vecteurs entiers SIMD efficaces comme x86 avec SSE2 ou AVX2.

C'est comme la réponse de @ NominalAnimal puisque nous avions tous les deux la même idée, mais vectorisés manuellement pour x86. (Et avec des nombres aléatoires de pire qualité, mais toujours probablement assez bons pour de nombreux cas d'utilisation.) Cela fonctionne environ 15 ou 30 fois plus rapidement que le code de @ Nominal, à ~ 13 Go/s de sortie ASCII sur un Processeur Intel Haswell 2,5 GHz avec AVX2. C'est encore moins que la bande passante maximale de la mémoire principale théorique (le DDR3-1600 double canal est d'environ 25,6 Go/s), mais je chronométrais l'écriture dans/dev/null, il s'agit donc simplement de réécrire un tampon qui reste chaud dans le cache. Skylake devrait exécuter ce même code beaucoup plus rapidement que Haswell (voir le bas de cette réponse).

En supposant que vous goulot d'étranglement sur les E/S sur le disque ou la canalisation quelque part, une implémentation rapide signifie que votre CPU n'a même pas besoin de cadencer plus haut que le ralenti. Il utilise beaucoup moins d'énergie totale pour produire le résultat. (Autonomie de la batterie/chaleur/réchauffement climatique.)

C'est si rapide que vous ne voulez probablement pas l'écrire sur le disque. Il suffit de recréer au besoin (à partir de la même graine si vous voulez à nouveau les mêmes données). Même si vous souhaitez l'alimenter dans un processus multi-thread qui peut utiliser tous les processeurs, l'exécuter pour lui rediriger les données le laissera chaud dans le cache L3 (et le cache L2 sur le noyau qui l'a écrit), et l'utiliser très peu de temps CPU. (Mais notez que la tuyauterie ajoute beaucoup de surcharge par rapport à l'écriture dans /dev/null. Sur un Skylake i7-6700k, la tuyauterie vers wc -c ou un autre programme qui lit + rejette simplement son entrée, il est environ 8 fois plus lent que l'écriture dans /dev/null , et n'utilise que 70% d'un processeur. Mais cela reste 4,0 Go/s sur un processeur à 3,9 GHz.

La régénérer est plus rapide que la relire, même à partir d'un SSD rapide connecté à PCIe, mais IDK s'il est plus économe en énergie (le multiplicateur vectoriel entier est assez occupé, et il est probablement assez gourmand en énergie, avec d'autres AVX2 256 b vecteur ALU). OTOH, je ne sais pas combien de temps CPU lire sur le disque enlèverait de quelque chose qui maximisait tous les cœurs traitant cette entrée. Je suppose qu'un changement de contexte pour régénérer en morceaux de 128k pourrait être compétitif avec l'exécution de code de système de fichiers/pagecache et l'allocation de pages pour lire les données du disque. Bien sûr, s'il fait déjà chaud dans la pagecache, c'est tout simplement memcpy. OTOH, nous écrivons déjà aussi vite que memcpy! (qui doit partager la bande passante de la mémoire principale entre la lecture et l'écriture). (Notez également que l'écriture dans une mémoire qui n'est pas encore chaude dans le cache déclenche généralement une lecture en vue de la propriété pour maintenir la cohérence du cache, ce qui peut être évité avec les magasins non temporels, ou avec le rep movsb de x86 (memcpy optimisé et memset en microcode, ce qui évite RFO, depuis l'implémentation d'Andy Glew dans P6 (Pentium Pro) )).


Jusqu'à présent, ce n'est qu'une preuve de concept, et la gestion de la nouvelle ligne n'est qu'approximativement correcte. Il ne va pas aux extrémités d'un tampon de puissance de 2. Avec plus de temps de développement. Je suis convaincu que je pourrais trouver un moyen plus efficace d'insérer des nouvelles lignes qui est également exactement correct, avec des frais généraux au moins aussi faibles que cela (par rapport à la sortie uniquement des espaces). Je pense que c'est quelque chose comme 10 à 20%. Je suis seulement intéressé à savoir à quelle vitesse nous pourrions faire cette course, pas à en avoir une version soignée, donc je laisserai cette partie comme un exercice pour le lecteur, avec des commentaires décrivant quelques idées.


Sur un Haswell i5 à son turbo max 2,5GHz, avec DDR3-1600MHz RAM , chronométré produisant 100GiB mais réduit. (Synchronisé sur cygwin64 sur Win10 avec gcc5.4 -O3 -march=native, omis -funroll-loops car j'avais du mal à obtenir des exécutions de synchronisation décentes sur cet ordinateur portable emprunté. Aurait dû simplement démarrer Linux sur un USB).

écrit dans/dev/null, sauf indication contraire.

  • James Hollis: (non testé)
  • Version fwrite de Nominal: ~ 2.21s
  • ceci (SSE2): ~ 0,142 s (temps non mis à l'échelle = réel = 14,232 s, utilisateur = 13,999 s, sys = 0,187 s).
  • ceci (AVX-128): ~ 0,140 s
  • ceci (AVX2): ~ 0,073 s (non mis à l'échelle: réel = 0m7,291s, utilisateur = 0m7,125s, sys = 0m0,155s).
  • cette tuyauterie cygwin (AVX2) vers wc -c, avec une taille de mémoire tampon de 128 Ko: 0,32 s avec un processeur à 2,38 GHz (turbo double cœur max). (temps non mis à l'échelle: réel = 32,466 s utilisateur = 11,468 s sys = 41,092 s, y compris à la fois ceci et wc). Cependant, seulement la moitié des données ont été copiées, car mon programme stupide suppose que l'écriture fait le tampon complet, même si ce n'est pas le cas et cygwin write () ne fait que 64 Ko par appel dans un canal.

Donc, avec SSE2, c'est environ 15 fois plus rapide que le code scalaire de @Nominal Animal. Avec AVX2, c'est environ 30 fois plus rapide. Je n'ai pas essayé une version du code de Nominal qui utilise simplement write() au lieu de fwrite(), mais probablement pour les gros tampons stdio reste généralement à l'écart. S'il copie les données, cela représenterait beaucoup de ralentissement.


Délais de production de 1 Go de données sur un Core2Duo E6600 (Merom 2,4 GHz, L1 privé 32 Ko, 4 Mo de cache partagé L2), DDR2-533 MHz dans Linux 4.2 64 bits (Ubuntu 15.10). Toujours en utilisant une taille de mémoire tampon de 128 Ko pour write (), nous n'avons pas exploré cette dimension.

écrit dans/dev/null, sauf indication contraire.

  • (SSE2) ceci avec la gestion de la nouvelle ligne et 4 vecteurs de chiffres de chaque vecteur d'octets aléatoires: 0,183s (chronométré faisant 100GiB en 18,3s, mais des résultats similaires pour les exécutions 1GiB). 1,85 instructions par cycle.
  • (SSE2) ceci, tuyauterie vers wc -c: 0,593 s (non mis à l'échelle: réel = 59,266 s utilisateur = 20,148 sys = 1 m 6,548 s, y compris le temps CPU du wc). Le même nombre d'appels système write () qu'avec cygwin, mais en fait en canalisant toutes les données car Linux gère les 128k d'un write () vers un tube.
  • Version fwrite() de NominalAnimal (gcc5.2 -O3 -march=native), exécutée avec ./decdig 100 $((1024*1024*1024/200)) > /dev/null: 3,19 s + 0,1%, avec 1,40 instruction par cycle. -funroll-loops fait peut-être une petite différence. clang-3.8 -O3 -march=native: 3,42 s +/- 0,1%
  • Nominal -fwrite canalisation vers wc -c: réel = 3,980 s utilisateur = 3,176 s sys = 2,080 s
  • Version ligne par ligne de James Hollis (clang++-3.8 -O3 -march=native): 22,885 s +/- 0,07%, avec 0,84 instructions par cycle. (g ++ 5,2 était légèrement plus lent: 22,98 s). Écrire une seule ligne à la fois a probablement fait beaucoup de mal.
  • Stéphane Chazelas tr < /dev/urandom | ...: real = 41.430s user = 26.832s sys = 40.120s. tr récupérait la plupart du temps un cœur de processeur, passant presque tout son temps dans le pilote du noyau à générer des octets aléatoires et à les copier dans un canal. L'autre cœur de cette machine à double cœur exécutait le reste du pipeline.
  • time LC_ALL=C head -c512M </dev/urandom >/dev/null: c'est-à-dire juste lire autant d'aléatoire sans tuyauterie: réel = 35.018s utilisateur = 0.036s sys = 34.940s.
  • Le programme Perl de Lưu Vĩnh Phúc (Perl v5.20.2 d'Ubuntu15.10):
    LANG=en_CA.UTF-8: réel = 4m32.634s utilisateur = 4m3.288s sys = 0m29.364.
    LC_ALL=C LANG=C: réel = 4m18.637s utilisateur = 3m50.324s sys = 0m29.356s. Encore très lent.

  • (SSE2) this sans gestion de la nouvelle ligne , et 3 ou 4 vecteurs de chiffres de chaque vecteur d'octets aléatoires (presque exactement la même vitesse: le Dig3 = v%10 étape est sur le point mort sur ce matériel): 0,166 s (1,82 instructions par cycle). Il s'agit essentiellement de la limite inférieure de ce que nous pouvons approcher avec une gestion de la nouvelle ligne parfaitement efficace.

  • (SSE2) Ancienne version de ceci sans gestion de nouvelle ligne, mais obtenant seulement un chiffre par élément uint16_t en utilisant v%10,/- 0,222 secondes + 0,4%, 2,12 instructions par cycle. (Compilé avec gcc5.2, -march=native -O3 -funroll-loops. Les boucles de déroulement aident à ce code sur ce matériel. Ne l'utilisez pas aveuglément, en particulier pour les gros programmes).
  • (SSE2) Ancienne version de ceci, écrit dans un fichier (sur un RAID10f2 de 3 disques durs magnétiques rapides, pas très optimisé pour les écritures): ~ 4 secondes. Pourrait aller plus vite en modifiant les paramètres du tampon d'E/S du noyau pour autoriser beaucoup plus de données sales avant les blocs write (). Le temps "système" est toujours ~ 1,0 seconde, beaucoup plus élevé que le temps "utilisateur". Sur cet ancien système avec une RAM DDR2-533 lente, il faut environ 4 fois plus de temps au noyau pour mémoriser les données dans la pagecache et exécuter les fonctions XFS que pour ma boucle pour continuer à les réécrire en place dans un tampon qui reste chaud dans cache.

Comment c'est fait

Un PRNG rapide est évidemment indispensable. xorshift128 + peut être vectorisé, vous avez donc deux ou quatre générateurs 64 bits en parallèle, dans les éléments d'un vecteur SIMD. Chaque étape produit un vecteur complet d'octets aléatoires. ( Implémentation AVX2 256b ici avec Intel intrinsèques ). Je l'ai choisi par rapport au choix de xorshift * de Nominal, car la multiplication vectorielle à 64 bits n'est possible que dans SSE2/AVX2 avec des techniques de précision étendue .


/ - Étant donné un vecteur d'octets aléatoires, nous pouvons découper chaque élément de 16 bits en plusieurs chiffres décimaux. Nous produisons plusieurs vecteurs d'éléments 16 bits qui sont chacun un ASCII chiffre + ASCII espace . Nous les stockons directement dans notre tampon de sortie.

Ma version originale vient d'utiliser x / 6554 pour obtenir un chiffre aléatoire de chaque élément uint16_t d'un vecteur. C'est toujours entre 0 et 9 inclus. Il est biaisé loin de 9, car (2^16 -1 ) / 6554 n'est que de 9,99923. (6554 = ceil ((2 ^ 16-1)/10), ce qui garantit que le quotient est toujours <10.)

x/6554 peut être calculé avec une multiplication par une constante "magique" ( la réciproque à virgule fixe ) et un décalage à droite du résultat de la moitié supérieure. C'est le meilleur cas pour la division par une constante; certains diviseurs prennent plus d'opérations et la division signée demande du travail supplémentaire. x % 10 a un biais similaire et n'est pas aussi bon marché à calculer. (La sortie asm de gcc est équivalente à x - 10*(x/10), c'est-à-dire une multiplication et une soustraction supplémentaires au-dessus de la division à l'aide d'un inverse multiplicatif modulaire.) De plus, le bit le plus bas de xorshift128 + n'est pas de qualité supérieure , donc diviser pour prendre l'entropie à partir de bits élevés est meilleur (pour la qualité et la vitesse) que modulo pour prendre l'entropie à partir de bits bas.

Cependant, nous pouvons utiliser une plus grande partie de l'entropie dans chaque uint16_t en regardant les chiffres décimaux bas, comme la fonction digit() de @ Nominal. Pour des performances maximales, j'ai décidé de prendre les 3 chiffres décimaux bas et x/6554, pour enregistrer un PMULLW et PSUBW (et probablement certains MOVDQA) par rapport à l'option de meilleure qualité de prendre les 4 chiffres décimaux bas. x/6554 est légèrement affecté par les 3 chiffres décimaux inférieurs, il existe donc une certaine corrélation entre les chiffres du même élément (séparation de 8 ou 16 chiffres dans la sortie ASCII, selon la largeur du vecteur).

Je pense que gcc divise par 100 et par 1000, plutôt que par une chaîne plus longue qui se divise successivement par 10, donc cela ne raccourcit probablement pas de manière significative la longueur de la chaîne de dépendance non portée par la boucle qui produit 4 résultats pour chaque PRNG production. port0 (multiplication et décalage vectoriels) est le goulot d'étranglement en raison des inverses multiplicatifs modulaires et des décalages dans xorshift +, il est donc certainement utile d'enregistrer une multiplication vectorielle.

xorshift + est si rapide que même en n'utilisant que ~ 3,3 bits de caractère aléatoire sur 16 (c'est-à-dire 20% d'efficacité) n'est pas beaucoup plus lent que de le découper en plusieurs chiffres décimaux. Nous ne faisons qu'approcher la distribution uniforme, car cette réponse est axée sur la vitesse tant que la qualité n'est pas trop mauvaise.

Tout type de comportement conditionnel qui conserve un nombre variable d'éléments demanderait beaucoup plus de travail. (Mais cela pourrait peut-être encore être fait de manière assez efficace en utilisant les techniques de compactage à gauche SIMD . Cependant, cela devient moins efficace pour les petites tailles d'éléments; les tables de recherche à masque aléatoire ne sont pas viables, et il n'y a pas Mélange AVX2 de franchissement de voie avec des éléments inférieurs à 32 bits. Une version 128b PSHUFB pourrait toujours être en mesure de générer un masque à la volée avec BMI2 PEXT/PDEP, comme vous pouvez le faire pour AVX2 avec des éléments plus grands , mais c'est délicat car un entier 64 bits ne contient que 8 octets. Le lien Godbolt sur cette réponse a du code qui pourrait fonctionner pour des nombres d'éléments plus élevés.)


Si la latence du RNG est un goulot d'étranglement, nous pourrions aller encore plus vite en exécutant deux vecteurs de générateurs en parallèle, en alternant celui que nous utilisons. Le compilateur peut toujours facilement garder tout dans les registres dans une boucle déroulée, ce qui permet aux deux chaînes de dépendances de fonctionner en parallèle.

Dans la version actuelle, en coupant la sortie du PRNG, nous goulot d'étranglement sur le débit du port 0, pas sur la latence PRNG, donc ce n'est pas nécessaire.


Le code: version AVX2

Version complète avec plus de commentaires sur l'explorateur du compilateur Godbolt .

Pas très bien rangé, désolé, je dois dormir et je veux que ce soit affiché.

Pour obtenir la version SSE2, s/_mm256/_mm, s/256/128/, s/v16u/v8u/ et remplacez vector_size(32) par 16. Modifiez également l'incrément de nouvelle ligne de 4 * 16 à 4 * 8. (Comme je l'ai dit, le code est compliqué et pas bien configuré pour compiler deux versions. Je n'avais pas prévu à l'origine de créer une version AVX2, mais je voulais vraiment tester sur un processeur Haswell auquel j'avais accès.)

#include <immintrin.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
//#include <string.h>

// This would work equally fast 128b or 256b at a time (AVX2):
// https://stackoverflow.com/questions/24001930/avx-sse-version-of-xorshift128
struct rngstate256 {
    __m256i state0;
    __m256i state1;
};

static inline __m256i xorshift128plus_avx2(struct rngstate256 *sp)
{
    __m256i s1 = sp->state0;
    const __m256i s0 = sp->state1;
    sp->state0 = s0;
    s1 = _mm256_xor_si256(s1, _mm256_slli_epi64(s1, 23));
    __m256i state1new = _mm256_xor_si256(_mm256_xor_si256(_mm256_xor_si256(s1, s0),
                            _mm256_srli_epi64(s1, 18)),
                      _mm256_srli_epi64(s0, 5));
    sp->state1 = state1new;
    return _mm256_add_epi64(state1new, s0);
}



// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));

__m256i* vec_store_digit_and_space(__m256i vec, __m256i *restrict p)
{
    v16u v = (v16u)vec;
    v16u ten = (v16u)_mm256_set1_epi16(10);

    v16u divisor = (v16u)_mm256_set1_epi16(6554);  // ceil((2^16-1) / 10.0)
    v16u div6554 = v / divisor;      // Basically the entropy from the upper two decimal digits: 0..65.
    // Probably some correlation with the modulo-based values, especially Dig3, but we do this instead of
    // Dig4 for more ILP and fewer instructions total.

    v16u Dig1 = v % ten;
    v /= ten;
    v16u Dig2 = v % ten;
    v /= ten;
    v16u Dig3 = v % ten;
    //  Dig4 would overlap much of the randomness that div6554 gets

    const v16u ascii_digitspace = (v16u)_mm256_set1_epi16( (' '<<8) | '0');

    v16u *vecbuf = (v16u*)p;
    vecbuf[0] = div6554 | ascii_digitspace;
    vecbuf[1] = Dig1    | ascii_digitspace;
    vecbuf[2] = Dig2    | ascii_digitspace;
    vecbuf[3] = Dig3    | ascii_digitspace;
    return p + 4;  // always a constant number of full vectors
}


void random_decimal_fill_buffer(char *restrict buf, size_t len, struct rngstate256 *restrict rngstate)
{
    buf = __builtin_assume_aligned(buf, 32);

    // copy to a local so clang can keep state in register, even in the non-inline version
    // restrict works for gcc, but apparently clang still thinks that *buf might alias *rngstate
    struct rngstate256 rng_local = *rngstate;

    __m256i *restrict p = (__m256i*restrict)buf;
    __m256i *restrict endbuf = (__m256i*)(buf+len);
    static unsigned newline_pos = 0;
    do {
        __m256i rvec = xorshift128plus_avx2(&rng_local);
        p = vec_store_digit_and_space(rvec, p);  // stores multiple ASCII vectors from the entropy in rvec

#if 1
        // this is buggy at the end or start of a power-of-2 buffer:
        // usually there's a too-short line, sometimes a too-long line
        const unsigned ncols = 100;
        newline_pos += 4*16;
        if (newline_pos >= ncols) {
            newline_pos -= ncols;
            char *cur_pos = (char*)p;
            *(cur_pos - newline_pos*2 - 1) = '\n';
        }
#endif
        // Turning every 100th space into a newline.
        // 1) With an overlapping 1B store to a location selected by a counter.  A down-counter would be more efficient
        // 2) Or by using a different constant for ascii_digitspace to put a newline in one element

        // lcm(200, 16) is 400 bytes, so unrolling the loop enough to produce two full lines makes a pattern of full vectors repeat
        // lcm(200, 32) is 800 bytes
        // a power-of-2 buffer size doesn't hold a whole number of lines :/
        // I'm pretty sure this can be solved with low overhead, like maybe 10% at worst.
    } while(p <= endbuf-3);

    *rngstate = rng_local;
}



#define BUFFER_SIZE (128 * 1024)
const static size_t bufsz = BUFFER_SIZE;
__attribute__((aligned(64))) static char static_buf[BUFFER_SIZE];

int main(int argc, char *argv[])
{
    // TODO: choose a seed properly.  (Doesn't affect the speed)
    struct rngstate256 xorshift_state = {
      _mm256_set_epi64x(123, 456, 0x123, 0x456),
      _mm256_set_epi64x(789, 101112, 0x789, 0x101112)
    };

    for (int i=0; i < 1024ULL*1024*1024 / bufsz * 100; i++) {
        random_decimal_fill_buffer(static_buf, bufsz, &xorshift_state);
        size_t written = write(1, static_buf, bufsz);
        (void)written;
        //fprintf(stderr, "wrote %#lx of %#lx\n", written, bufsz);
    }

}

Compilez avec gcc, clang ou ICC (ou, espérons-le, tout autre compilateur qui comprend le dialecte C GNU de C99 et les caractéristiques intrinsèques d'Intel). GNU Les extensions de vecteur C sont très pratiques pour que le compilateur génère les nombres magiques pour la division/modulo en utilisant des inverses multiplicatifs modulaires, et des __attribute__ s occasionnels sont utiles.

Cela pourrait être écrit de manière portable, mais cela prendrait plus de code.


Notes de performance:

Le magasin de chevauchement pour insérer des nouvelles lignes a une surcharge importante pour décider où le placer (erreurs de prédiction de branche et goulots d'étranglement frontaux sur Core2), mais le magasin lui-même n'a aucun impact sur les performances. Commentant simplement cette instruction de stockage dans l'asm du compilateur (en laissant toutes les branches identiques), les performances sur Core2 sont restées inchangées, les exécutions répétées donnant le même temps à + moins de 1%. Je conclus donc que le tampon/cache de stockage le gère très bien.

Pourtant, utiliser une sorte de fenêtre tournante de ascii_digitspace avec un élément ayant une nouvelle ligne pourrait être encore plus rapide, si nous déroulons suffisamment pour que les compteurs/branchements disparaissent.


L'écriture dans/dev/null est fondamentalement un no-op, donc le tampon reste probablement chaud dans le cache L2 (256 Ko par cœur sur Haswell). Une accélération parfaite des vecteurs 128b aux vecteurs 256b est attendue: il n'y a pas d'instructions supplémentaires, et tout (y compris les magasins) se produit avec deux fois la largeur. Cependant, la branche d'insertion de nouvelle ligne est prise deux fois plus souvent. Malheureusement, je n'ai pas chronométré sur ma configuration Haswell cygwin avec cette partie #ifdef ed out.

/ - 2,5 GHz * 32B/13,7 Go/s = 5,84 cycles par magasin AVX2 sur Haswell. C'est assez bien, mais ça pourrait être plus rapide. Il y a peut-être des frais généraux dans les appels du système cygwin que je ne le pensais. Je n'ai pas essayé de les commenter dans la sortie asm du compilateur (ce qui garantirait que rien ne soit optimisé.)

Le cache L1 peut supporter un magasin 32B par horloge, et L2 n'est pas une bande passante beaucoup plus faible (latence plus élevée, cependant).

Lorsque j'ai regardé IACA il y a quelques versions (sans branchement pour les nouvelles lignes, mais seulement un vecteur ASCII par vecteur RNG), il prédisait quelque chose comme un magasin de vecteurs 32B pour 4 ou 5 horloges.

J'espérais obtenir plus d'accélération en extrayant plus de données de chaque résultat RNG, basé sur l'examen du asm moi-même, compte tenu des guides d'Agner Fog et d'autres ressources d'optimisation auxquelles j'ai ajouté des liens pour dans le wiki de balise SO x86 .)

Ce serait probablement beaucoup plus rapide sur Skylake , où la multiplication et le décalage des entiers vectoriels peuvent s'exécuter sur deux fois plus de ports (p0/p1) par rapport à Haswell (p0 uniquement). xorshift et l'extraction de chiffres utilisent tous deux beaucoup de décalages et de multiplications. ( Mise à jour: Skylake l'exécute à 3.02 IPC, ce qui nous donne 3,77 cycles par magasin AVX2 de 32 octets , chronométré à 0,030 s par itération de 1 Go, écrivant dans /dev/null sur Linux 4.15 sur i7-6700k à 3,9 GHz.


Il ne nécessite pas de mode 64 bits pour bien fonctionner . La version SSE2 est tout aussi rapide lorsqu'elle est compilée avec -m32, car elle n'a pas besoin de très nombreux registres vectoriels, et tous les calculs 64 bits sont effectués dans des vecteurs, pas des registres à usage général.

Il est en fait légèrement plus rapide en mode 32 bits sur Core2, car la macro-fusion de comparaison/branche ne fonctionne qu'en mode 32 bits, donc il y a moins d'uops pour le noyau hors service (18,3 s (1,85 instructions par horloge) vs 16,9 s (2,0 IPC)). La taille de code plus petite de l'absence de préfixe REX aide également les décodeurs de Core2.

De plus, certains mouvements vectoriels reg-reg sont remplacés par des charges, car toutes les constantes ne se corrigent plus dans les regs vectoriels. Étant donné que le débit de charge du cache L1 n'est pas un goulot d'étranglement, cela aide réellement. (par exemple en multipliant par un vecteur constant de set1(10): movdqa xmm0, xmm10/pmullw xmm0, xmm1 se transforme en movdqa xmm0, [constant]/pmullw xmm0, xmm1.) Puisque reg-reg MOVDQA nécessite un port ALU, il est en concurrence avec le travail réel effectué, mais une charge MOVDQA ne concurrence que la bande passante de décodage frontale. (Avoir une adresse de 4 octets dans de nombreuses instructions annule une grande partie du gain de l'enregistrement des préfixes REX.

Je ne serais pas surpris si la sauvegarde des uops ALU MOVDQA est la source des gains réels, car le frontend devrait plutôt bien suivre la moyenne de 2.0 IPC.

Toutes ces différences disparaissent sur Haswell, où le tout devrait s'exécuter à partir du cache décodé-uop, sinon du tampon de bouclage. La macro-fusion de branche ALU + fonctionne dans les deux modes depuis Nehalem.

18
Peter Cordes

Voici une solution que j'espère simple à comprendre:

od -An -x /dev/urandom | tr -dc 0-9 | fold -w100 | awk NF=NF FS= | head -c1G
  • od crée un flux uniforme de chiffres hexadécimaux à partir de /dev/random.
  • tr se débarrasse des lettres, ne gardant que 0-9 chiffres
  • fold garantit qu'il y a 100 chiffres par ligne
  • awk insère des espaces à l'intérieur des lignes
  • head tronque l'entrée à 1 gigaoctet
14
sam hocevar

Vous pouvez utiliser la commande jot pour cela:

jot -r 50000000 0 9 | fmt -w 200 > output.txt
6
gardenhead

C'est similaire à la méthode de Stéphane Chazelas, mais j'ai lu 64 bits à la fois pour améliorer les performances. La distribution est toujours uniforme mais maintenant vous obtenez 19 chiffres pour chaque 8 octets au lieu de seulement 8 dans le meilleur des cas comme avant

Perl -nle 'BEGIN{$/=\8; $,=" "}
           $n = unpack("Q");
           next if $n >= 10000000000000000000;
           $s = sprintf("%019u", $n);
           Push @a, (split //, $s);
           if (@a >= 100) {print (splice @a, 0, 100);}' < /dev/urandom | head -c1G

Sur une plateforme 32 bits, 9 chiffres seront lus à chaque fois au lieu de 19.

6
phuclv

Je suis en quelque sorte d'accord avec Nominal Animal pour utiliser un langage de programmation compilé si vous avez besoin de vitesse. Cependant, vous n'avez pas à écrire votre propre code RNG en C. C++ 11 offre l'excellent Mersenne Twister dans le cadre de sa bibliothèque standard.

#include <time.h>
#include <random>
#include <iostream>
using namespace std;

int main() {
    mt19937 gen(time(0)); 
    uniform_int_distribution<> dist(0,9);

    for(int j=0; j<5000000; j++){
        for (int i = 0; i < 99; i++) {  
            cout << dist(gen) << " ";
        }  
        cout << dist(gen) << endl;
    }
    return 0;
}

Le code ci-dessus est relativement simple et prend environ une minute lorsque je redirige la sortie vers un fichier. Nous pouvons aller beaucoup plus vite en créant une chaîne assez grande pour 100 chiffres et en piratant les chiffres. Cela nous permet d'appeler cout chaque ligne plutôt que chaque chiffre.

#include <time.h>
#include <random>
#include <iostream>
using namespace std;

int main() {
    mt19937 gen(time(0)); 
    uniform_int_distribution<> dist(0,9);

    char line[201];
    for(int i=1; i<199; i++)
        line[i] = ' ';
    line[199] = '\n';
    line[200] = 0;

    for(int j=0; j<5000000; j++){
        for (int i = 0; i < 199; i += 2) {  
            line[i] = dist(gen)+'0';
        }  
        cout << line;
    }
    return 0;
}

Ce code prend environ six secondes à ma machine. N'oubliez pas qu'il s'agit d'une sortie standard, alors dirigez-la vers un fichier.

J'ai quelques avertissements. Tout d'abord, j'écris ceci sur un PC Windows. Je pense que les bibliothèques sont toutes présentes sur Linux, mais si je me trompe, assurez-vous de le signaler.

En outre, il génère en fait exactement un demi-milliard de chiffres séparés par un espace, ce qui est techniquement un gigaoctet mais peut-être pas exactement ce que vous vouliez. Il produit 5 millions de lignes, 100 chiffres par ligne. Si la différence est importante, vous pouvez augmenter le nombre de lignes. Sur ma boîte Windows, le fichier semble être légèrement supérieur à 10 ^ 9 octets, ce qui, je pense, est lié à des caractères de nouvelle ligne supplémentaires.

3
James Hollis

Cela dépend de votre définition de "aléatoire". Si vous voulez dire aléatoire sur le plan cryptographique, il vous suffit d'obtenir une bonne bibliothèque et de mordre la balle, d'attendre qu'elle s'exécute.

Si vous avez juste besoin de quelque chose d'assez aléatoire, voici un moyen simple:

  1. Obtenez un fichier de plusieurs Go. Votre film préféré sera bon.
  2. Gzip it, un moyen facile d'extraire des motifs répétés
  3. Parcourez le fichier un nybble (un demi-octet) à la fois. Chaque valeur sera comprise entre 0 et 15. Jetez moins de 1 ou plus de 10. Soustrayez 1 de chacun des premiers milliards de survivants et écrivez-le sous forme de chiffre.

Cela peut prendre une heure pour fonctionner sur une machine lente; assez rapide et assez aléatoire pour la plupart des usages.

1
Malvolio
#!/bin/bash
FILE_CREAT='/tmp/testfile'
MAX_SIZE=$(( 1 * 1024 * 1024 ))
rm -rf ${FILE_CREAT}
while true
do
    STRING=''
    for (( i = 0 ; i < 100 ; i++ ))
    do
        NUM_RAN=$(cat /dev/urandom | tr -dc 0-9 | head -c 1)
        if [ $i -eq 0 ]
        then
            STRING=${NUM_RAN}
        else
            STRING=${STRING}' '${NUM_RAN}
        fi
    done
    echo ${STRING} >> $FILE_CREAT
    FILE_SIZE=$(du -s ${FILE_CREAT} | awk '{print $1}')
    if [ ${FILE_SIZE} -ge ${MAX_SIZE} ]
    then
        break
    fi
done
exit $1
1
NamNT