Qual è il modo più veloce per generare un file di testo da 1 GB contenente cifre casuali?


52

Ho provato uno script bash, ma ci è voluto troppo tempo per creare un semplice file da 1 MB. Penso che la risposta stia nell'utilizzare /dev/randomo /dev/urandom, ma altri post qui mostrano solo come aggiungere tutti i tipi di dati a un file usando queste cose, ma voglio aggiungere solo numeri.

Quindi, c'è un comando che posso usare per creare un file casuale di dimensioni 1 GB contenente solo numeri compresi tra 0 e 9?

Modifica: voglio che l'output sia qualcosa del genere

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

L'intervallo è compreso tra 0 e 9 e indica solo i numeri 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Inoltre, ho bisogno che siano separati da spazio e 100 per riga, fino al nnumero di righe. Questo n è qualcosa che non mi interessa, voglio che la mia dimensione finale sia di 1 GB.

Modifica: sto usando Ubuntu 16.04 LTS



21
Probabilmente dovresti dire cosa intendi per "casuale" - forza crittografica casuale, o una sequenza pseudo-casuale è adeguata?
Toby Speight,

4
@posixKing: Nota che, sebbene la mia risposta sia decisamente ironica, in realtà non sto suggerendo di scrivere un programma C per tale compito! - se si generano regolarmente insiemi di dati così enormi o li si generano spesso, l'approccio potrebbe farti risparmiare tempo. (Sul mio laptop, genera 1 GB di cifre separate da spazio in circa dieci secondi.) Tuttavia, se si tratta di una tantum, non pensare nemmeno di scrivere un programma C per questo (a meno che non ti piaccia la programmazione, e considera questo pratica o simili); i comandi e le utilità della shell eseguono il compito in meno tempo e sforzi complessivi.
Animale nominale,

7
Questo è abbastanza veloce e conforme a RFC 1149.5:yes 4 | tr '\n' ' ' | fold -w 200 | head -c1G
Matthew Crumley,

Risposte:


38

Questa è in parte una risposta ironica, a causa del titolo della domanda.

Quando cerchi "il modo più veloce per ..." , la risposta è quasi sempre uno strumento specializzato. Questa "risposta" mostra uno di questi strumenti, solo così puoi sperimentare.

Questa non è una risposta seria, perché non dovresti cercare strumenti specializzati per lavori che fai una volta sola o molto raramente. Vedrai, finirai per passare più tempo a cercare strumenti e apprenderli, piuttosto che a fare cose. Conchiglie e utilità come bashe awknon sono le più veloci, ma di solito è possibile scrivere una riga per ottenere il lavoro, spendendo solo pochi secondi. perlPossono anche essere usati linguaggi di scripting migliori come , anche se la curva di apprendimento perlè ripida, e esito a raccomandarlo per tali scopi, perché sono stato traumatizzato da terribili progetti perl. pythond'altra parte è leggermente handicappato dal suo I / O piuttosto lento; tuttavia è solo un problema quando si filtrano o si generano gigabyte di dati.

In ogni caso, il seguente programma di esempio C89 (che utilizza POSIX.1 per un clock di maggiore precisione solo se disponibile) dovrebbe raggiungere una velocità di generazione di circa 100 MB / s (testato su Linux su un laptop con un processore Intel i5-4200U, eseguendo il piping dell'output a /dev/null), usando un generatore di numeri pseudo-casuale abbastanza buono. (L'output dovrebbe superare tutti i test di BigCrunch, ad eccezione del test MatrixRank, poiché il codice utilizza xorshift64 * e il metodo di esclusione per evitare di distorcere le cifre.)

decimale 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;
}

Possiamo renderlo molto più veloce, se passiamo a un buffer di linea, e fwrite()una volta invece di emettere ogni cifra alla volta. Tieni presente che il flusso è ancora completamente bufferizzato, per evitare scritture parziali (senza potenza di due) se l'output è un dispositivo a blocchi.

#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;
}

Nota: entrambi gli esempi sono stati modificati il ​​18-11-2016 per garantire una distribuzione uniforme delle cifre (zero è escluso; vedere ad esempio qui per il confronto e i dettagli su vari generatori di numeri pseudo-casuali).

Compilare usando ad esempio

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

e opzionalmente installare a livello di sistema per l' /usr/binutilizzo

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

Prende il numero di cifre per riga e il numero di righe. Perché 1000000000 / 100 / 2 = 5000000(cinque milioni; byte totali divisi per colonne divise per 2), è possibile utilizzare

./decimal-digits 100 5000000 > digits.txt

per generare le dimensioni digits.txtin gigabyte come desiderato dall'OP.

Si noti che il programma stesso è scritto più con leggibilità che efficienza in mente. Il mio intento qui non è quello di mostrare l'efficienza del codice - utilizzerei comunque POSIX.1 e I / O di basso livello, piuttosto che interfacce C generiche - ma per farti vedere facilmente che tipo di equilibrio c'è con lo sforzo speso nello sviluppo di strumenti dedicati rispetto alle loro prestazioni, rispetto agli one-liner o agli scriptlet a shell corta o awk.

Usando la libreria GNU C, chiamare la fputc()funzione per ogni output di caratteri comporta un overhead molto piccolo (di una chiamata di funzione indiretta, o condizionali - l' FILEinterfaccia è in realtà piuttosto complessa e versatile, vedi). Su questo particolare laptop Intel Core i5-4200U, reindirizzando l'output a /dev/null, la prima versione (fputc) richiede circa 11 secondi, mentre la versione line-at-a-time richiede solo 1,3 secondi.

Mi capita spesso di scrivere tali programmi e generatori solo perché mi piace giocare con enormi set di dati. Sono strano in quel modo. Ad esempio, una volta ho scritto un programma per stampare tutti i valori IEEE-754 a virgola mobile positivi finiti in un file di testo, con una precisione sufficiente per ottenere lo stesso valore esatto quando analizzato. Il file aveva dimensioni di alcuni gigabyte (forse circa 4G); non ci sono molti positivi finiti floatcome si potrebbe pensare. L'ho usato per confrontare le implementazioni che leggono e analizzano tali dati.

Per i casi d'uso normali, come l'OP sta avendo, gli script della shell e gli scriptlet e le battute singole rappresentano l'approccio migliore. Meno tempo impiegato per svolgere l'attività complessiva. (Tranne se hanno bisogno di un file diverso ogni giorno o così, o ci sono molte persone che hanno bisogno di un file diverso, in cui - raro - caso, uno strumento dedicato come sopra, potrebbe giustificare lo sforzo speso.)


Sì, probabilmente mmap()è la via più semplice per raggiungere la migliore velocità I / O, ma fai un benchmark prima di fare qualsiasi reclamo!
Toby Speight,

@TobySpeight: in Linux, l'I / O di basso livello, ovvero l'utilizzo write(), è in genere più veloce di mmap(). fwrite()non è molto più lento. Sì, l'ho confrontato (non solo per questo esempio particolare); write()in grossi blocchi (262144, 524288 o 1048576 byte) tende a sovraperformare gli altri metodi. La versione fputc()implementata nella libreria GNU C (che ho anche ampiamente confrontato) è lenta per una serie di motivi; in particolare, l'implementazione deve eseguire salti condizionati o chiamate indirette per ogni personaggio aggiunto; quel leggero sovraccarico sostenuto così spesso si somma.
Animale nominale

Solo per interesse - hai fatto un confronto delle prestazioni con le altre risposte?
Trauma digitale,

2
@DigitalTrauma: li ho appena eseguiti per te, reindirizzando l'output a /dev/null. La sceneggiatura di Stéphane Chazelas dura circa 52 secondi; frammento di perl (incluso il headfiltraggio) circa 58 secondi; lo shufsnippet (con una tempistica corretta; si misura solo il tempo shuf, supponendo che la pasta non impiegherà più tempo) impiega circa 69 secondi. C ++ 11 di James Hollis un programma line-at-a-time dura 14 secondi. Il programma sopra richiede 10 secondi.
Animale nominale

3
(Ho perso il treno di pensieri sopra, scusate.) Il punto è, scegliendo l'algoritmo giusto - il PRNG sufficientemente casuale ma molto veloce qui -, ha prodotto un aumento della velocità di quasi un ordine di grandezza (10 ×). (L'ultima versione dei miei programmi è circa 40 volte più veloce dei frammenti shell o perl.) Questo è tipico. Forse avrei dovuto enfatizzare la scelta dell'algoritmo giusto quando scrivevo un programma più nella mia risposta sopra? (D'altra parte, questa non è una domanda di programmazione, ma una domanda Unix / Linux, su come generare molte cifre.)
Nominal Animal

81

Questo:

 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

(supponendo headun'implementazione che supporti -c) sembra essere abbastanza veloce sul mio sistema.

trtraduce l'intero intervallo di byte (da 0 a 255, da 0 a 0377 in ottale): i primi 25 byte come 0, i 25 successivi come 1 ... quindi 25 9 il resto (da 250 a 255) a "x" che poi scartare (con tr -d x) come vogliamo una distribuzione uniforme (supponendo che /dev/urandomabbia una distribuzione uniforme stessa) e quindi non dare un orientamento ad alcune cifre.

Ciò produce una cifra per il 97% dei byte di /dev/urandom. fold -w 1lo rende una cifra per riga. paste -sviene chiamato con un elenco di separatori composto da 99 caratteri di spazio e un carattere di nuova riga, in modo da avere 100 cifre separate da spazio su ciascuna riga.

head -c1Gotterrà il primo GiB (2 30 ) di quello. Si noti che l'ultima riga verrà troncata e non delimitata. Potresti troncare a 2 30 -1 e aggiungere manualmente la newline mancante, oppure troncare a 10 9 byte invece che è 50 milioni di quelle 200 linee di byte (lo head -n 50000000renderebbe anche un comando standard / portatile).

Questi tempi (ottenuti da zshun sistema quad-core) forniscono un'indicazione di dove viene impiegato il tempo della CPU:

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

Il primo trè il collo di bottiglia, la maggior parte del tempo trascorso nel kernel (suppongo per la generazione di numeri casuali). La tempistica è approssimativamente in linea con la velocità da cui posso ottenere byte /dev/uramdom(circa 19 MiB / se qui produciamo 2 byte per ogni 0,97 byte di / dev / urandom a una velocità di 32 MiB / s). foldsembra passare una quantità irragionevole di tempo CPU (15s) solo per inserire un carattere di nuova riga dopo ogni byte, ma ciò non influisce sul tempo complessivo in quanto funziona su una CPU diversa nel mio caso (l'aggiunta -bdell'opzione lo rende leggermente più efficiente, dd cbs=1 conv=unblocksembra un'alternativa migliore).

Puoi eliminare head -c1Ge raderti qualche secondo impostando un limite sulla dimensione del file ( limit filesize 1024mcon zsho ulimit -f "$((1024*1024))"con la maggior parte delle altre shell (incluso zsh)) invece in una subshell.

Ciò potrebbe essere migliorato se estraessimo 2 cifre per ogni byte, ma per questo avremmo bisogno di un approccio diverso. Quanto sopra è molto efficiente perché trcerca ogni byte in un array di 256 byte. Non può farlo per 2 byte alla volta e l'utilizzo di cose del genere hexdump -e '1/1 "%02u"'calcola la rappresentazione testuale di un byte utilizzando algoritmi più complessi sarebbe più costoso della generazione di numeri casuali stessa. Tuttavia, se come nel mio caso, hai dei core della CPU il cui tempo libero, potrebbe comunque riuscire a radersi qualche secondo:

Con:

< /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

Ottengo (nota comunque che qui sono 1.000.000.000 di byte invece di 1.073.741.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

Più tempo complessivo della CPU, ma meglio distribuito tra i miei 4 core della CPU, quindi finisce con meno tempo di clock. Il collo di bottiglia è ora hexdump.

Se utilizziamo al ddposto della linea fold, possiamo effettivamente ridurre la quantità di lavoro hexdumpda svolgere e migliorare l'equilibrio tra le 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')" -

(qui assumendo GNU ddper il suo iflag=fullblocke status=none) che dà:

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

Torna alla generazione di numeri casuali come il collo di bottiglia.

Ora, come sottolineato da @OleTange, se si dispone openssldell'utilità, è possibile utilizzarla per ottenere un generatore pseudo-casuale di byte più veloce (specialmente sui processori che hanno istruzioni AES).

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

sul mio sistema sputa 15 volte più byte al secondo di /dev/urandom. (Non posso commentare come si paragona in termini di fonte di casualità crittograficamente sicura se questo si applica al tuo caso d'uso).

</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')" -

Ora dà:

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

tornando ad hexdumpessere il collo di bottiglia.

Dato che ho ancora CPU da vendere, posso eseguirne 3 hexdumpin parallelo.

</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')" -

( <&3è necessario per shell diverse da quelle zshche chiudono i comandi 'stdin on / dev / null quando eseguite in background).

Adesso scendono a 6,2 secondi e le mie CPU sono quasi completamente utilizzate.


3
Ho appena cancellato la mia risposta precedente e ho votato per questa. Non ho ricevuto alcuni dei requisiti. Bella risposta tra l'altro.
Marcelo,

3
perché non generi più cifre per ogni passaggio? Anche se leggi byte per byte sei comunque in grado di produrre 2 cifre ogni volta
phuclv

@ LưuVĩnhPhúc, ho rimosso la perlvariante che era significativamente più lenta comunque. Non riesco a ottenere 2 cifre per byte con quell'approccio tr | fold | paste.
Stéphane Chazelas,

Sono afk o lo proverei io stesso, ma potresti provare a convertire 42 byte alla volta in 100-102 cifre usando bc(quindi rilasciare le cifre 0, 1 o 2 più significative).
Eric Towers,

gitlab.com/ole.tange/tangetools/tree/master/rand genera 1-4 GB pseudocasuali al secondo (qualità AES).
Ole Tange,

23

Se hai a shufdisposizione (coreutils GNU recenti) puoi farlo:

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

Sulla mia macchina virtuale, questo è un po 'più lento della risposta di Stéphane di un fattore 3: 4.


shufsul mio PC aziendale non ha -r, fmtnon ha -ganche
phuclv

2
@ LưuVĩnhPhúc Yep - YMMV. Ho scoperto che core-utils versione 8.25 ha questi, ma 8.4 no. Quale versione stai usando?
Trauma digitale,

1
Sto usando coreutils 8.13
phuclv

@ StéphaneChazelas intelligente paste/ printftrucco - grazie. La tua risposta ora è apparentemente più veloce.
Trauma digitale,

17

Se non hai bisogno di casualità di altissima qualità e la distribuzione quasi uniforme è abbastanza buona, puoi andare molto veloce, specialmente su una CPU moderna con efficienti vettori interi SIMD come x86 con SSE2 o AVX2.

Questa è la risposta di @NominalAnimal poiché entrambi abbiamo avuto la stessa idea, ma vettorializzati manualmente per x86. (E con numeri casuali di qualità peggiore, ma probabilmente abbastanza buono per molti casi d'uso.) Funziona circa 15 o 30 volte più veloce del codice di @ Nominal, a ~ 13 GB / s di output ASCII su un Intel Haswell a 2,5 GHz CPU con AVX2. Questo è ancora inferiore alla larghezza di banda massima teorica della memoria principale (DDR3-1600 a doppio canale è circa 25,6 GB / s), ma stavo programmando la scrittura su / dev / null, quindi in realtà è solo riscrittura di un buffer che rimane caldo nella cache. Skylake dovrebbe eseguire lo stesso codice in modo significativamente più veloce di Haswell (vedere il fondo di questa risposta).

Supponendo che in realtà si verifichino colli di bottiglia sull'I / O su disco o esegua il piping da qualche parte, un'implementazione rapida significa che la CPU non deve nemmeno avere un clock superiore al minimo. Utilizza molta meno energia totale per produrre il risultato. (Durata batteria / riscaldamento / riscaldamento globale.)

È così veloce che probabilmente non vuoi scriverlo su disco. Rigenerare appena necessario (dallo stesso seme se si desidera ripetere gli stessi dati). Anche se vuoi alimentarlo a un processo multi-thread che può usare tutte le CPU, eseguirlo per reindirizzare i dati a esso lascerà caldo nella cache L3 (e cache L2 sul core che lo ha scritto), e usa così tanto poco tempo della CPU. (Ma nota che il piping aggiunge un sacco di overhead rispetto alla scrittura /dev/null. Su uno Sky7ke i7-6700k, il piping su wc -co un altro programma che legge + scarta il suo input, è circa 8 volte più lento rispetto alla scrittura/dev/null e usa solo il 70% di un CPU, ma è ancora 4,0 GB / s su una CPU da 3,9 GHz.

Rigenerarlo è più veloce che rileggerlo anche da un veloce SSD collegato a PCIe, ma IDK se è più efficiente dal punto di vista energetico (il moltiplicatore di vettore intero è abbastanza impegnato e probabilmente ha molta fame, insieme ad altri AVX2 256b ALU vettoriali). OTOH, non so quanto tempo di lettura della CPU dal disco richiederebbe da qualcosa che stava esaurendo tutti i core che elaborano questo input. Immagino che un cambio di contesto per la rigenerazione in blocchi di 128k potrebbe essere competitivo con l'esecuzione di codice filesystem / pagecache e l'allocazione di pagine per leggere i dati dal disco. Ovviamente, se è già caldo nel pagecache, è semplicemente memcpy. OTOH, scriviamo già velocemente come memcpy! (che deve dividere la larghezza di banda della memoria principale tra lettura e scrittura). (Nota anche che scrivere in memoria che 'rep movsb(memcpy ottimizzato e memset nel microcodice, che evita la RFO, poiché l'implementazione di Andy Glew in P6 (Pentium Pro) )).


Finora questa è solo una prova di concetto e la gestione di newline è solo approssimativamente corretta. È sbagliato attorno alle estremità di un buffer di potenza di 2. Con più tempo di sviluppo. Sono fiducioso di poter trovare un modo più efficiente per inserire nuove righe che sia anche esattamente corretto, con spese generali almeno così basse (rispetto alla produzione di soli spazi). Penso che questo sia tra il 10 e il 20%. Sono solo interessato a sapere quanto velocemente potremmo farcela, non in realtà averne una versione raffinata, quindi lascerò quella parte come esercizio per il lettore, con commenti che descrivono alcune idee.


Su una Haswell i5 al suo turbo massimo di 2,5 GHz, con RAM DDR3-1600 MHz , temporizzata producendo 100 GiB ma ridotta. (Cronometrato su cygwin64 su Win10 con gcc5.4 -O3 -march=native, omesso -funroll-loopsdal momento che non riuscivo a ottenere tempi decenti su questo laptop preso in prestito. Avrei dovuto avviare Linux su una USB).

scrivendo a / dev / null se non diversamente specificato.

  • James Hollis's: (non testato)
  • La versione fwrite di Nominal: ~ 2.21s
  • questo (SSE2): ~ 0.142s (tempi non scalati = reale = 14.232s, utente = 13.999s, sys = 0.187s).
  • questo (AVX-128): ~ 0.140s
  • questo (AVX2): ~ 0.073s (non scalato : reale = 0m7.291s, utente = 0m7.125s, sys = 0m0.155s).
  • questo (AVX2) piping cygwin a wc -c, con dimensioni del buffer di 128 kB: 0,32 s con CPU a 2,38 GHz (turbo dual-core massimo). (tempi non scalati: reale = 32.466s utente = 11.468s sys = 41.092s, inclusi sia questo che wc). Tuttavia, solo la metà dei dati è stata effettivamente copiata, poiché il mio programma stupido presuppone che write esegua il buffer completo, anche se non è così e cygwin write () esegue solo 64k per chiamata in una pipe.

Quindi con SSE2 questo è circa 15 volte più veloce del codice scalare di @Nominal Animal. Con AVX2, è circa 30 volte più veloce. Non ho provato una versione del codice di Nominal che utilizza solo write()invece di fwrite(), ma presumibilmente per buffer di grandi dimensioni stdio rimane per lo più fuori dai piedi. Se sta copiando i dati, ciò comporterebbe molto rallentamento.


Tempi per la produzione di 1 GB di dati su un Core2Duo E6600 (Merom 2.4GHz, L1 privato da 32 kB, cache L2 condivisa da 4MiB), DDR2-533MHz in Linux 4.2 a 64 bit (Ubuntu 15.10). Usando ancora una dimensione del buffer di 128 kB per write (), non ho esplorato quella dimensione.

scrivendo a / dev / null se non diversamente specificato.

  • (SSE2) questo con gestione newline e 4 vettori di cifre da ciascun vettore di byte casuali: 0,183s (temporizzato facendo 100GiB in 18,3s, ma risultati simili per esecuzioni da 1GiB). 1,85 istruzioni per ciclo.
  • (SSE2) questo, piping a wc -c: 0,593s (non scalato: reale = 59.266s user = 20.148s sys = 1m6.548s, incluso il tempo di CPU del wc). Stesso numero di chiamate di sistema write () come con cygwin, ma in realtà esegue il piping di tutti i dati perché Linux gestisce tutti i 128k di un write () su una pipe.
  • NominalAnimal's fwrite()version (gcc5.2 -O3 -march=native), eseguito con ./decdig 100 $((1024*1024*1024/200)) > /dev/null: 3.19s +/- 0.1%, con 1.40 istruzioni per ciclo. -funroll-loop ha fatto forse una piccola differenza. clang-3.8 -O3 -march=native: 3,42s +/- 0,1%
  • fwriteTubazioni nominali a wc -c: real = 3.980s user = 3.176s sys = 2.080s
  • La versione line-at-a-time di James Hollis ( clang++-3.8 -O3 -march=native): 22,885s +/- 0,07%, con 0,84 istruzioni per ciclo. (g ++ 5.2 era leggermente più lento: 22.98s). Scrivere solo una riga alla volta probabilmente fa molto male.
  • Stéphane Chazelas's tr < /dev/urandom | ...: real = 41.430s user = 26.832s sys = 40.120s. trstava facendo tutto da solo un core della CPU per la maggior parte del tempo, impiegando quasi tutto il suo tempo nel driver del kernel generando byte casuali e copiandoli in una pipe. L'altro core su questa macchina dual core stava eseguendo il resto della pipeline.
  • time LC_ALL=C head -c512M </dev/urandom >/dev/null: cioè solo leggendo tanta casualità senza piping: real = 35.018s user = 0.036s sys = 34.940s.
  • Programma perl di Lưu Vĩnh Phúc (perl v5.20.2 da Ubuntu15.10)
    LANG=en_CA.UTF-8:: real = 4m32.634s user = 4m3.288s sys = 0m29.364.
    LC_ALL=C LANG=C: real = 4m18.637s user = 3m50.324s sys = 0m29.356s. Ancora molto lento.

  • (SSE2) questo senza gestione di newline , e 3 o 4 vettori di cifre da ciascun vettore di byte casuali (quasi esattamente la stessa velocità: il dig3 = v%10passo riguarda il pareggio su questo HW): 0,166s (1,82 istruzioni per ciclo) . Questo è fondamentalmente il limite inferiore per ciò a cui possiamo avvicinarci con una gestione newline perfettamente efficiente.

  • (SSE2) Vecchia versione di questo senza gestione newline, ma ottenendo solo una cifra per elemento uint16_t utilizzando v%10, 0,222 secondi +/- 0,4%, 2,12 istruzioni per ciclo. (Compilato con gcc5.2,. I -march=native -O3 -funroll-loopscicli di srotolamento possono aiutare per questo codice su questo hardware. Non utilizzarlo ciecamente, specialmente per programmi di grandi dimensioni).
  • (SSE2) Vecchia versione di questo, scrivendo su un file (su un RAID10f2 di 3 dischi rigidi magnetici veloci, non molto ottimizzato per le scritture): ~ 4 secondi. Potrebbe andare più veloce modificando le impostazioni del buffer I / O del kernel per consentire molti più dati sporchi prima dei blocchi write (). Il tempo "Sistema" è ancora ~ 1,0 secondi, molto più lungo del tempo "utente". Su questo vecchio sistema con RAM DDR2-533 lenta, il kernel impiega circa 4 volte più tempo a memorizzare i dati nella pagecache ed eseguire le funzioni XFS rispetto al mio ciclo per continuare a riscriverli sul posto in un buffer che rimane caldo in cache.

Come è fatto

Un PRNG veloce è ovviamente essenziale. xorshift128 + può essere vettorializzato, in modo da avere due o quattro generatori a 64 bit in parallelo, in elementi di un vettore SIMD. Ogni passaggio produce un vettore completo di byte casuali. ( Implementazione AVX2 256b qui con Intel intrinsics ). L'ho scelto per la scelta di xorshift di Nominal *, perché la moltiplicazione di numeri interi a 64 bit è possibile solo in SSE2 / AVX2 con tecniche di precisione estesa .


Dato un vettore di byte casuali, possiamo tagliare ogni elemento a 16 bit in più cifre decimali. Produciamo più vettori di elementi a 16 bit che sono ciascuno una cifra ASCII + spazio ASCII . Lo memorizziamo direttamente nel nostro buffer di output.

La mia versione originale ha usato solo x / 6554per ottenere una cifra casuale da ogni elemento uint16_t di un vettore. È sempre compreso tra 0 e 9, inclusi. È distorto 9, perché (2^16 -1 ) / 6554è solo 9.99923. (6554 = ceil ((2 ^ 16-1) / 10), che assicura che il quoziente sia sempre <10.)

x/6554può essere calcolato con un moltiplicatore per una costante "magica" ( il reciproco a punto fisso ) e uno spostamento a destra del risultato della metà alta. Questo è il caso migliore per la divisione per una costante; alcuni divisori prendono più operazioni e la divisione firmata richiede un lavoro extra. x % 10ha una propensione simile e non è così economico da calcolare. (L'output asm di gcc equivale a x - 10*(x/10), cioè un moltiplicare e sottrarre in cima alla divisione usando un inverso moltiplicativo modulare.) Inoltre, il bit più basso di xorshift128 + non è così di alta qualità , quindi è meglio dividere l'entropia dai bit alti ( per qualità e velocità) rispetto al modulo per prendere l'entropia dai bit bassi.

Tuttavia, possiamo usare più entropia in ogni uint16_t osservando le cifre decimali basse, come la digit()funzione di @ Nominal . Per le massime prestazioni, ho deciso di prendere le 3 cifre decimali basse e x/6554, di salvare un PMULLW e PSUBW (e probabilmente un po 'di MOVDQA) rispetto all'opzione di qualità superiore di prendere le 4 cifre decimali basse. x / 6554 è leggermente influenzato dalle 3 cifre decimali basse, quindi esiste una certa correlazione tra le cifre dello stesso elemento (separazione di 8 o 16 cifre nell'uscita ASCII, a seconda della larghezza del vettore).

Penso che gcc si stia dividendo per 100 e per 1000, piuttosto che una catena più lunga che si divide successivamente per 10, quindi probabilmente non sta accorciando significativamente la lunghezza della catena di dipendenze non a ciclo che produce 4 risultati da ogni uscita PRNG. port0 (moltiplicare e spostare vettore) è il collo di bottiglia a causa delle inversioni moltiplicative modulari e degli spostamenti in xorshift +, quindi è sicuramente utile salvare una moltiplicazione vettoriale.

xorshift + è così veloce che anche usando solo ~ 3,3 bit di casualità ogni 16 (ovvero un'efficienza del 20%) non è molto più lento di tagliarlo in più cifre decimali. Approssimiamo solo la distribuzione uniforme, perché questa risposta è focalizzata sulla velocità purché la qualità non sia troppo male.

Qualsiasi tipo di comportamento condizionale che mantiene un numero variabile di elementi richiederebbe molto più lavoro. (Ma potrebbe ancora essere fatto in qualche modo in modo efficiente utilizzando le tecniche di imballaggio a sinistra SIMD . Tuttavia, questo diventa meno efficiente per elementi di piccole dimensioni; le tabelle di ricerca shuffle-mask giganti non sono vitali e non c'è shuffle di attraversamento di corsia AVX2 con dimensioni inferiori a 32- elementi bit. Una versione PSHUFB a 128b potrebbe ancora essere in grado di generare una maschera al volo con BMI2 PEXT / PDEP, come puoi fare per AVX2 con elementi più grandi , ma è difficile perché un numero intero a 64 bit contiene solo 8 byte. su quella risposta ha un codice che potrebbe funzionare per conteggi di elementi più elevati.)


Se la latenza dell'RNG è un collo di bottiglia, potremmo andare ancora più veloci eseguendo due vettori di generatori in parallelo, alternando quello che usiamo. Il compilatore può ancora facilmente tenere tutto nei registri in un ciclo non srotolato e ciò consente alle due catene di dipendenze di funzionare in parallelo.

Nella versione attuale, tagliando l'output del PRNG, in realtà abbiamo colli di bottiglia sul throughput della porta 0, non sulla latenza del PRNG, quindi non è necessario.


Il codice: versione AVX2

Versione completa con altri commenti sull'esploratore del compilatore Godbolt .

Non molto ordinato, mi dispiace che devo dormire e voglio che questo sia pubblicato.

Per ottenere la versione SSE2, s/_mm256/_mm, s/256/128/, s/v16u/v8u/, e il cambiamento vector_size(32)a 16. cambiare anche l'incremento di nuova riga da 4 * 16-4 * 8. (Come ho detto, il codice è disordinato e non ben impostato per la compilazione di due versioni. Inizialmente non avevo intenzione di creare una versione AVX2, ma poi volevo davvero testare su una CPU Haswell a cui avevo accesso.)

#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);
    }

}

Compilare con gcc, clang o ICC (o, auspicabilmente, qualsiasi altro compilatore che comprenda il dialetto C GNU C di C99 e le caratteristiche intrinseche di Intel). Le estensioni vettoriali GNU C sono molto utili per far sì che il compilatore generi i numeri magici per divisione / modulo usando inversioni moltiplicative modulari __attribute__e sono utili occasionali .

Questo potrebbe essere scritto in modo portabile, ma richiederebbe più codice.


Note sulle prestazioni:

Il negozio sovrapposto per l'inserimento di nuove righe ha un notevole sovraccarico per decidere dove posizionarlo (previsioni errate delle filiali e colli di bottiglia del frontend su Core2), ma il negozio stesso non ha alcun impatto sulle prestazioni. Commentare solo le istruzioni del negozio nell'asm del compilatore (lasciando tutte le ramificazioni uguali) ha lasciato le prestazioni su Core2 completamente invariate, con corse ripetute che danno lo stesso tempo a +/- meno dell'1%. Quindi concludo che il buffer / cache del negozio lo gestisce bene.

Tuttavia, l'utilizzo di una sorta di finestra rotante ascii_digitspacecon un elemento con una nuova linea potrebbe essere ancora più veloce, se srotoliamo abbastanza da far scomparire eventuali contatori / ramificazioni.


Scrivere su / dev / null è sostanzialmente un no-op, quindi il buffer probabilmente rimane caldo nella cache L2 (256 kB per core su Haswell). È previsto lo speedup perfetto da vettori 128b a vettori 256b: non ci sono istruzioni aggiuntive e tutto (compresi i negozi) avviene con una larghezza doppia. Tuttavia, il ramo di inserimento della nuova riga viene eseguito due volte più spesso. Sfortunatamente non ho avuto il tempo di installare Cygwin Haswell con quella parte #ifdef.

2,5 GHz * 32 B / 13,7 GB / s = 5,84 cicli per negozio AVX2 su Haswell. È abbastanza buono, ma potrebbe essere più veloce. Forse c'è qualche sovraccarico nelle chiamate di sistema di Cygwin di quanto pensassi. Non ho provato a commentare quelli nell'output asm del compilatore (il che garantirebbe che nulla fosse ottimizzato).

La cache L1 può supportare un archivio di 32 B per clock e L2 non ha una larghezza di banda molto inferiore (latenza più elevata, tuttavia).

Quando ho guardato IACA alcune versioni fa (senza la diramazione per le nuove linee, ma ottenendo solo un vettore ASCII per vettore RNG), stava predicendo qualcosa come un archivio vettoriale da 32 B per 4 o 5 orologi.

Speravo di ottenere una maggiore accelerazione dall'estrazione di più dati da ogni risultato RNG, basato sull'osservazione di me stesso, considerando le guide di Agner Fog e altre risorse di ottimizzazione per cui ho aggiunto collegamenti nel wiki SO x86 .)

Probabilmente sarebbe significativamente più veloce su Skylake , dove moltiplicare e spostare i numeri interi di vettore può funzionare su un numero di porte doppio (p0 / p1) rispetto a Haswell (solo p0). xorshift e l'estrazione delle cifre utilizzano entrambi molti turni e moltiplicazioni. ( Aggiornamento: Skylake lo esegue a 3.02 IPC, dandoci 3,77 cicli per archivio AVX2 a 32 byte , temporizzato a 0,030 secondi per iterazione da 1 GB, scrivendo /dev/nullsu Linux 4.15 su i7-6700k a 3,9 GHz.


Non richiede la modalità a 64 bit per funzionare bene . La versione SSE2 è altrettanto veloce quando compilata -m32, perché non ha bisogno di molti registri vettoriali e tutta la matematica a 64 bit viene eseguita in vettori, non in registri di uso generale.

In realtà è leggermente più veloce in modalità 32-bit su Core2, perché la macro-fusione comparativa / diramazione funziona solo in modalità 32-bit, quindi ci sono meno uops per il core fuori servizio (18,3 s (1,85 istruzioni per clock) vs 16.9s (2.0 IPC)). La dimensione del codice inferiore rispetto alla mancanza di prefissi REX aiuta anche i decodificatori di Core2.

Inoltre, alcuni movimenti di vettori reg-reg vengono sostituiti con carichi, poiché non tutte le costanti si fissano più nei registri vettori. Poiché il throughput di caricamento dalla cache L1 non è un collo di bottiglia, questo in realtà aiuta. (ad es. moltiplicando per un vettore costante di set1(10): movdqa xmm0, xmm10/ pmullw xmm0, xmm1trasforma in movdqa xmm0, [constant]/ pmullw xmm0, xmm1.) Poiché il reg-reg MOVDQA richiede una porta ALU, compete con il lavoro reale svolto, ma un carico MOVDQA compete solo per la larghezza di banda della decodifica front-end. (Avere un indirizzo di 4 byte all'interno di molte istruzioni annulla gran parte del guadagno derivante dal salvataggio dei prefissi REX.

Non sarei sorpreso se il salvataggio di ALU MOVDQA uops è da dove provengono i guadagni reali, dal momento che il frontend dovrebbe tenere il passo con la media di 2.0 IPC abbastanza bene.

Tutte queste differenze scompaiono su Haswell, dove l'intera cosa dovrebbe essere eseguita dalla cache decoded-uop, se non dal buffer di loopback. La macro-fusione ALU + branch funziona in entrambe le modalità da Nehalem.


6
Adoro il modo in cui sei entrato in "modalità bestia" nell'argomento! :) Ancora più importante, è un eccellente esempio di che tipo di guadagni sono disponibili, se hai davvero bisogno o vuoi ottenere le massime prestazioni, utilizzando una conoscenza di livello molto basso dell'hardware a portata di mano. Inoltre, stiamo usando solo un thread qui; la maggior parte dei processori Intel / AMD desktop e server (e anche quelli ARM in tablet e SBC leggeri) hanno più core, quindi ci sono ancora più accelerazioni nel tempo reale disponibili. E, infine, quanto siano impraticabili le domande "il modo più veloce per" , a causa del semplice sforzo richiesto.
Animale nominale

1
@NominalAnimal: Sì, anche un quad ARM o octo core lento potrebbe facilmente saturare la larghezza di banda della memoria principale facendo la stessa cosa con NEON (anche se fossero collegati al veloce DDR3 a doppio canale), se ha SIMD interi e spostamenti a 64 bit . Suppongo che NEON abbia moltiplicazioni di dimensioni degli elementi a 16 bit per il lavoro audio. La programmazione delle istruzioni sarebbe molto più impegnativa per un ARM in ordine, perché ogni iterazione della catena di dipendenze trasportata in loop (xorshift128 +) alimenta alcune catene di dipendenze indipendenti di troncare quella e farla archiviare in memoria ...
Peter Cordes,

... L'esecuzione fuori servizio la consuma a colazione, perché il tutto è abbastanza breve da far rientrare diverse iterazioni nel ROB (192 uops su HSW IIRC). (vale a dire la "finestra" delle istruzioni visualizzate dall'esecuzione fuori ordine include più iterazioni). Quindi la CPU può terminare l'archivio finale per 2 o 3 iterazioni fa mentre inizia anche all'inizio dell'iterazione corrente. Ciò nasconde la latenza delle catene indipendenti, quindi conta solo la velocità effettiva. Su un core in ordine, ciò richiederebbe il pipelining del software ...
Peter Cordes,

... Un buon compilatore ARM dovrebbe fare un po 'di questo per te se lo scrivi con intrinseci (o sintassi vettoriale nativa GNU C per tutto, come avrei dovuto fare io in primo luogo). Non ho alcuna esperienza con quello per davvero, quindi potresti aver bisogno di massaggiare il tuo loop e forse fare un po 'di srotolamento manuale / pipeline di software nella fonte per ottenere una buona asm. (Esistono alcuni core ARM fuori servizio, presenti nei telefoni di fascia alta, ma non hanno una finestra fuori servizio così grande come Haswell. OTOH, hanno anche una velocità di picco inferiore, quindi c'è meno per guadagnare trovando più ILP).
Peter Cordes,

1
@NominalAnimal: anche, concordato sulla stupidità della domanda. "Il più veloce" senza restrizioni sulla qualità della casualità è sciocco ... Con BTRFS, gli stessi dati sul disco possono far parte di un file più volte (vedere EXTENT_SAME in 4.2 ). Quindi puoi generare un 4kB casuale o 1MB e ripeterlo. È casualità con un breve periodo, ma è ancora casuale e costa solo I / O di metadati. (In realtà, è necessario che il blocco termini con una nuova riga. Mcm (4096, 4096 * 200) = 4096 * 200 = 819200 = 800 kB, quindi il blocco di ripetizione è un multiplo di quello.)
Peter Cordes

14

Ecco una soluzione che spero sia semplice da capire:

od -An -x /dev/urandom | tr -dc 0-9 | fold -w100 | awk NF=NF FS= | head -c1G
  • odcrea un flusso uniforme di cifre esadecimali da /dev/random.
  • trsi sbarazza delle lettere, mantenendo solo le 0-9cifre
  • fold assicura che ci siano 100 cifre per riga
  • awk inserisce spazi all'interno di linee
  • head tronca l'ingresso a 1 gigabyte

2
Questo è un buon modo alternativo per produrre più di una cifra per byte di / dev / random pur avendo una distribuzione uniforme, che produce in media 320 cifre per ogni 256 byte di / dev / urandom (meno di quando si convertono byte <200 modulo Da 100 a decimale che ti dà 400 cifre per ogni 256 byte).
Stéphane Chazelas,

6

È possibile utilizzare il jotcomando per questo:

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

1
@DigitalTrauma La mia versione di fmtnon ha un'opzione di larghezza obiettivo. Ad ogni modo, sarà esatto perché tutte le cifre occupano esattamente una colonna!
gardenhead

Per la cronaca, la mia fmtversione è fmt (GNU coreutils) 8.25(Ubuntu 16.04)
Digital Trauma,

2
il numero giusto per mezzo GB è: 1024 * 1024 * 1024/2 =536870912
Olivier Dulac,

1
@OlivierDulac Dipende da quale "gigabyte" stai parlando. Alcune persone usano 1 Gb per indicare 10 ^ 9 invece di 2 ^ 30, anche se tecnicamente non è corretto. Inoltre mi piacciono i bei numeri rotondi :)
gardenhead,

6
@gardenhead, sempre più persone ora tendono a spostarsi su Gigabyte == 1e9 e Gibibyte == 2 ^ 30 poiché questa è la definizione standard IEC. Vedi Wikipedia . Si noti che Gb stesso avrebbe preferito essere giga po ' .
Stéphane Chazelas,

6

Questo è simile al metodo di Stéphane Chazelas, tuttavia ho letto subito 64 bit per migliorare le prestazioni. La distribuzione è ancora uniforme ma ora ottieni 19 cifre per ogni 8 byte anziché solo 8 nel migliore dei casi come prima

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

Sulla piattaforma a 32 bit verranno lette 9 cifre ogni volta anziché 19.


Ciò può sollevare un'eccezione se il tuo sistema non supporta numeri interi a 64 bit o perlnon è compilato con il supporto quad.
cuonglm,

@cuonglm sì, come ho detto, se perl non ha 64 bit su quel sistema, il programma deve essere modificato next if $n >= 1000000000; $s = sprintf("%09u", $n);per ottenere solo 9 cifre
phuclv,

Non puoi, il programma andrà in crash $n = unpack("Q")se quad non è supportato.
cuonglm,

1
@cuonglm cambia BEGIN{$/=\4; $,=" "} $n = unpack("L");anche in
phuclv

1
Siamo spiacenti, ma questo ottiene 19 cifre da 8 byte di input solo circa il 54,2% delle volte e nessuna il resto, con una media di 1,29 cifre per byte di input. Se più come Stephane usi <16e18e dividi per 16, ottieni 18 cifre 86,7% per 1,95 dpB. Con 32 bit, <4e9 /4ottiene 9 cifre del 93,1% per 2,10 dpB. Ma 5 byte (come esadecimale (H10)) <1e12danno 12 cifre al 90,9% per 2,18 dpB, o dividendo l'esagono a metà e facendo ogni metà si <1e6 ottiene 6 cifre al 95,4% per 2,29 dpB; questo si avvicina al limite di log_10 (256) = 2.41.
dave_thompson_085

3

Sono abbastanza d'accordo con Nominal Animal nell'usare un linguaggio di programmazione compilato se hai bisogno di velocità. Tuttavia, non è necessario scrivere il proprio codice RNG in C. C ++ 11 offre l'eccellente Mersenne Twister come parte della sua libreria 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;
}

Il codice sopra è ragionevolmente semplice e richiede circa un minuto quando installo l'output in un file. Possiamo andare molto più velocemente creando una stringa abbastanza grande per 100 cifre e hackerandole. Questo ci consente di chiamare cout ogni linea anziché ogni cifra.

#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;
}

Questo codice impiega la mia macchina in circa sei secondi. Ricorda che è un output standard, quindi esegui il pipe in un file.

Ho un paio di dichiarazioni di non responsabilità. Innanzitutto, sto scrivendo questo su un PC Windows. Penso che le librerie siano tutte presenti su Linux, ma se sbaglio, assicurati di segnalarlo.

Inoltre, genera in realtà esattamente mezzo miliardo di cifre separate da spazio, il che è tecnicamente un gigabyte ma forse non è esattamente quello che volevi. Produce 5 milioni di righe, 100 cifre per riga. Se la differenza è importante, puoi aumentare il numero di righe. Sulla mia finestra di Windows il file sembra essere leggermente più grande di 10 ^ 9 byte, che penso abbia a che fare con i caratteri di nuova riga extra.


2
Ehi, la critica non è proprio giusta! :) La maggior parte del mio programma è l'analisi dei parametri della riga di comando. Se ometto anche commenti, controlli degli errori e hardcode il numero di colonne e righe di output, posso renderlo meno del doppio delle dimensioni del tuo codice - quasi mostruoso . :) Scherzi a parte: Sì, le librerie sono disponibili nella maggior parte delle distribuzioni Linux. Sul mio laptop, la linea per volta dura circa 14 secondi, mentre la mia versione per linea dura solo 1,3 secondi. La differenza è dovuta solo al PRNG: Mersenne Twister è molto più lenta di Xorshift64 *.
Animale nominale

1
C'è una cosa pratica che vorrei sottolineare che ti sei perso, ma spero che tu non lo prenda come negatività, solo qualcosa a cui pensare: come ho già detto nella mia risposta, i programmi one-shot raramente valgono la pena tempo impiegato per scrivere. Ecco perché l'aggiunta dell'analisi da riga di comando e il testo di utilizzo della guida sono quasi sempre utili. Ho una vasta gamma di tali programmi di utilità, e piuttosto che cacciare le loro fonti per scoprire cosa fa ciascuno di essi, li eseguo solo, quindi me lo diranno; e posso modificare il loro comportamento abbastanza da soddisfare più di un bisogno. Ammortamento dei costi di sviluppo.
Animale nominale

@NominalAnimal un'altra cosa importante è che hai convogliato l'output a /dev/nullcui sarebbe molto più veloce della scrittura su un file reale
phuclv,

@ LưuVĩnhPhúc: Beh, non proprio. Questo lappy ha un SSD Samsung da 128 GB, con letture e scritture sequenziali di ~ 500 MB / s. Mettine quattro in una configurazione software-RAID0 Linux, e otterrai ben più di un gigabyte al secondo che legge e scrive quando si generano set di dati così grandi (stima circa 1,75 TB / s). 1 GB / s è stato raggiunto anni fa con 12 unità SATA (piatti rotanti, nemmeno SSD) con Linux sw-RAID0. (Nota: byte / s, non bit / s.) Certo, sembra sciocco per una macchina "normale", ma quelli che giocano con set di dati di grandi dimensioni, lo trovano utile - ti fai perdere tempo da tutto ciò che fai (con set di dati di grandi dimensioni) quel modo.
Animale nominale

1
@NominalAnimal e Lu'u: Ancora più importante, se hai abbastanza RAM, il programma può uscire ben prima che i dati siano tutti sul disco. La maggior parte del lavoro in una write()chiamata di sistema di grandi dimensioni è un memcpy nella pagecache, che si blocca solo se il kernel decide di farlo invece di allocare più spazio nel buffer. Questo programma dovrebbe essere un collo di bottiglia sull'I / O del disco solo quando la memoria è stretta o se si era utilizzato O_DIRECT per bypassare la pagecache. Se write()in blocchi di dimensioni inferiori alla cache, si spera che i tuoi dati vadano nella memoria principale solo una volta e il buffer riscritto sul posto rimanga attivo nella cache L2 o L3.
Peter Cordes,

1

Dipende dalla tua definizione di "casuale". Se intendi crittograficamente casuale, devi solo ottenere una buona libreria e mordere il proiettile, attendere che venga eseguito.

Se hai solo bisogno di qualcosa che sembra abbastanza casuale, ecco un modo semplice:

  1. Ottieni un file lungo diversi GB. Il tuo film preferito sarà buono.
  2. Comprimilo, un modo semplice per spremere schemi ripetuti
  3. Passare attraverso il file un nybble (mezzo byte) alla volta. Ogni valore sarà compreso tra 0 e 15. Getta via qualsiasi meno di 1 o maggiore di 10. Sottrai 1 da ciascuno dei primi miliardi di sopravvissuti e scrivilo come una cifra.

Potrebbe volerci un'ora per funzionare su una macchina lenta; abbastanza veloce e abbastanza casuale per la maggior parte degli scopi.


9
/dev/urandomè probabilmente migliore di gzip, sia in velocità che in casualità.
Stig Hemmer,

Get a file that is several Gb longavrete bisogno di un file ** almeno 8Gb` per ottenere un file di 1 GB
phuclv

1
#!/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
Benvenuti nel sito! Vedi i collegamenti sulla pagina del mio profilo. Ci sono molti problemi qui che vedo quasi universalmente negli script di shell, ma ciò non li rende corretti.
Wildcard il

2
@Wildcard: mai cat file | trquando puoi tr <file. IIRC, puoi anche <file tr. Pensavo stessi parlando di questo script di shell apparentemente goffo e lento, come du | awkdopo ogni riga per controllare le dimensioni e riaprire il file per aggiungere ogni riga invece di reindirizzare al di fuori del ciclo.
Peter Cordes,

2
@PeterCordes, sì. Perché usare un loop di shell per elaborare il testo è considerato una cattiva pratica? è particolarmente rilevante: questo script si basa sull'idea che Bash è un linguaggio di programmazione come C, che non lo è. Ma, \ @NamNT, spero che resti su questo sito perché è chiaro che hai una mente molto logica. :)
Wildcard il

4
@PeterCordes cat /dev/urandom | busy-cmdè uno di quei rari casi in cui può avere senso in quanto può dividere la generazione casuale e il cmd occupato tra processori. Non tanto per il tr, ma odper esempio fa la differenza per Sam .
Stéphane Chazelas,

1
@ StéphaneChazelas: oh giusto !! Sì, la chiamata di sistema read () è dove viene impiegato il tempo della CPU RNG.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.