¿Cuál es la forma más rápida de generar un archivo de texto de 1 GB que contenga dígitos aleatorios?


52

Probé un script bash, pero me llevó demasiado tiempo crear un archivo simple de 1 MB. Creo que la respuesta está en usar /dev/randomo /dev/urandom, pero otras publicaciones aquí solo muestran cómo agregar todo tipo de datos a un archivo usando estas cosas, pero quiero agregar solo números.

Entonces, ¿hay un comando que pueda usar para crear un archivo aleatorio de tamaño 1 GB que contenga solo números entre 0 y 9?

Editar: quiero que la salida sea algo como esto

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

El rango es 0 - 9, lo que significa solo números 0, 1, 2, 3, 4, 5, 6, 7, 8 y 9. También necesito que estén separados por espacios y 100 por línea, hasta el nnúmero de líneas. Esto es algo que no me importa, quiero que mi tamaño final sea de 1 GB.

Editar: estoy usando Ubuntu 16.04 LTS



21
Probablemente debería decir lo que quiere decir con "aleatorio": fuerza criptográfica aleatoria, o ¿es adecuada una secuencia pseudoaleatoria?
Toby Speight

44
@posixKing: Tenga en cuenta que, aunque mi respuesta es definitivamente irónica, ¡en realidad no estoy sugiriendo escribir un programa en C para tal tarea! -, si genera habitualmente conjuntos de datos tan grandes, o los genera a menudo, el enfoque puede ahorrarle tiempo. (En mi computadora portátil, genera 1 GB de dígitos separados por espacios en unos diez segundos). Sin embargo, si esto es único, ni siquiera piense en escribir un programa C para esto (a menos que le guste la programación, y considere esto práctica o tal); Los comandos y las utilidades del shell logran la tarea en menos tiempo y esfuerzo total invertido.
Nominal Animal

77
Esto es bastante rápido y cumple con RFC 1149.5:yes 4 | tr '\n' ' ' | fold -w 200 | head -c1G
Matthew Crumley

Respuestas:


38

Esto es parcialmente una respuesta irónica, debido al título de la pregunta.

Cuando busca "la forma más rápida de ..." , la respuesta es casi siempre una herramienta especializada. Esta "respuesta" muestra una de esas herramientas, para que pueda experimentar.

Esta no es una respuesta seria, porque no debe buscar herramientas especializadas para trabajos que solo realiza una vez, o muy raramente. Verá, terminará pasando más tiempo buscando herramientas y aprendiendo sobre ellas, en lugar de hacer cosas. Los shells y las utilidades les gustan bashy awkno son los más rápidos, pero por lo general puedes escribir una sola línea para lograr el trabajo, gastando solo unos segundos. perlTambién se pueden utilizar mejores lenguajes de scripting , aunque la curva de aprendizaje perles empinada, y dudo en recomendarlo para tales propósitos, porque me han traumatizado los terribles proyectos de Perl. pythonpor otro lado, está ligeramente perjudicado por su E / S bastante lenta; Sin embargo, solo es un problema cuando filtra o genera gigabytes de datos.

En cualquier caso, el siguiente programa de ejemplo C89 (que usa POSIX.1 para un reloj de mayor precisión solo si está disponible) debería alcanzar una tasa de generación de aproximadamente 100 MB / s (probado en Linux en una computadora portátil con un procesador Intel i5-4200U, canalizando la salida a /dev/null), utilizando un generador de números pseudoaleatorios bastante bueno. (La salida debe pasar todas las pruebas BigCrunch, excepto la prueba MatrixRank, ya que el código usa xorshift64 * y el método de exclusión para evitar sesgos en los dígitos).

dígitos-decimales.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;
}

Podemos hacerlo mucho más rápido si cambiamos a un búfer de línea, y fwrite()una vez en lugar de generar cada dígito a la vez. Tenga en cuenta que todavía mantenemos el flujo completamente protegido, para evitar escrituras parciales (sin potencia de dos) si la salida es un dispositivo de bloque.

#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: ambos ejemplos se editaron el 18-11-2016 para garantizar una distribución uniforme de los dígitos (se excluye el cero; consulte, por ejemplo, aquí para ver una comparación y detalles sobre varios generadores de números pseudoaleatorios).

Compilar usando por ejemplo

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

y opcionalmente instalar todo el sistema para /usr/binusar

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

Toma el número de dígitos por línea y el número de líneas. Porque 1000000000 / 100 / 2 = 5000000(cinco millones; bytes totales divididos por columnas divididas por 2), puede usar

./decimal-digits 100 5000000 > digits.txt

para generar el gigabyte del tamaño digits.txtdeseado por el OP.

Tenga en cuenta que el programa en sí está escrito más teniendo en cuenta la legibilidad que la eficiencia. Mi intención aquí no es mostrar la eficiencia del código; de todos modos, usaría POSIX.1 y E / S de bajo nivel, en lugar de interfaces C genéricas, sino permitirle ver fácilmente qué tipo de equilibrio hay con el esfuerzo invertido en el desarrollo de herramientas dedicadas en comparación con su rendimiento, en comparación con líneas simples o shell corto o scriptlets awk.

Al usar la biblioteca GNU C, llamar a la fputc()función para cada salida de caracteres conlleva una sobrecarga muy pequeña (de una llamada de función indirecta, o condicionales; la FILEinterfaz es en realidad bastante compleja y versátil, ya ves). En esta computadora portátil Intel Core i5-4200U en particular, la redirección de la salida a /dev/nullla primera versión (fputc) demora aproximadamente 11 segundos, mientras que la versión de línea a la vez demora solo 1.3 segundos.

A menudo escribo tales programas y generadores solo porque me gusta jugar con grandes conjuntos de datos. Soy raro de esa manera. Por ejemplo, una vez escribí un programa para imprimir todos los valores positivos finitos de coma flotante IEEE-754 en un archivo de texto, con suficiente precisión para producir exactamente el mismo valor cuando se analiza. El archivo tenía unos pocos gigabytes de tamaño (quizás 4G más o menos); no hay tantos positivos finitos floatcomo uno podría pensar. Utilicé esto para comparar implementaciones que leen y analizan dichos datos.

Para casos de uso normal, como el que tiene OP, los scripts de shell y scriptlets y one-liners son el mejor enfoque. Menos tiempo dedicado a realizar la tarea general. (Excepto si necesitan un archivo diferente cada día más o menos, o si hay muchas personas que necesitan un archivo diferente, en el que, en casos excepcionales, una herramienta dedicada como la anterior, podría justificar el esfuerzo invertido).


Sí, probablemente mmap()sea ​​la ruta más fácil hacia la mejor velocidad de E / S, ¡pero es un punto de referencia antes de hacer cualquier reclamo!
Toby Speight

@TobySpeight: en Linux, las E / S de bajo nivel, es decir write(), el uso , suelen ser más rápidas que mmap(). fwrite()No es mucho más lento. Sí, lo he comparado (solo que no para este ejemplo en particular); write()en fragmentos grandes (262144, 524288 o 1048576 bytes) tiende a superar a los otros métodos. La versión de fputc()implementado en la biblioteca GNU C (que también he comparado ampliamente) es lenta por varias razones; en particular, la implementación tiene que hacer saltos condicionales o llamadas indirectas para cada carácter agregado; esa ligera sobrecarga incurrida tan a menudo se suma.
Nominal Animal

Solo por interés: ¿ha hecho una comparación de rendimiento con las otras respuestas?
Trauma digital

2
@ DigitalTrauma: Acabo de ejecutarlos para usted, redirigiendo la salida a /dev/null. El guión de Stéphane Chazelas tarda unos 52 segundos; fragmento de perl (incluido el headfiltrado) unos 58 segundos; su shuffragmento (con el tiempo correcto; solo mide el tiempo de shuf, suponiendo que la pasta no demore más) toma aproximadamente 69 segundos. El programa C ++ 11 de James Hollis, línea por línea, lleva 14 segundos. El programa anterior lleva 10 segundos.
Nominal Animal

3
(Perdí mi línea de pensamiento anterior, lo siento). El punto es que, eligiendo el algoritmo correcto , el PRNG suficientemente aleatorio pero muy rápido aquí, produjo un aumento de velocidad de casi un orden de magnitud (10 ×). (La última versión de mis programas es aproximadamente 40 veces más rápida que los fragmentos de shell o perl). Esto es típico. ¿Quizás debería haber enfatizado la elección del algoritmo correcto al escribir un programa más en mi respuesta anterior? (Por otro lado, esta no es una pregunta de programación, sino una pregunta de Unix / Linux, sobre cómo generar muchos dígitos.)
Nominal Animal

81

Esta:

 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

(suponiendo una headimplementación que soporte -c) parece ser razonablemente rápido en mi sistema.

trtraduce todo el rango de bytes (0 a 255, 0 a 0377 en octal): los 25 primeros bytes como 0, los 25 siguientes como 1 ... luego 25 9 el resto (250 a 255) a "x" que luego descarte (con tr -d x) ya que queremos una distribución uniforme (suponiendo que /dev/urandomtenga una distribución uniforme en sí) y, por lo tanto, no proporcione un sesgo a algunos dígitos.

Eso produce un dígito para el 97% de los bytes de /dev/urandom. fold -w 1lo hace un dígito por línea. paste -sse llama con una lista de separadores que consta de 99 caracteres de espacio y un carácter de nueva línea, para tener 100 dígitos separados por espacio en cada línea.

head -c1Gobtendrá el primer GiB (2 30 ) de eso. Tenga en cuenta que la última línea se truncará y no se delimitará. Podría truncar a 2 30 -1 y agregar la nueva línea que falta a mano, o truncar a 10 9 bytes, que es 50 millones de esas líneas de 200 bytes ( head -n 50000000también lo haría un comando estándar / portátil).

Estos tiempos (obtenidos zshen un sistema de cuatro núcleos), dan una indicación de dónde se gasta el tiempo de 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

El primero tres el cuello de botella, la mayor parte del tiempo pasado en el núcleo (supongo que para la generación de números aleatorios). El tiempo está más o menos en línea con la velocidad de la que puedo obtener bytes /dev/uramdom(aproximadamente 19MiB / sy aquí producimos 2 bytes por cada 0,97 bytes de / dev / urandom a una velocidad de 32MiB / s). foldparece estar gastando una cantidad irrazonable de tiempo de CPU (15s) solo para insertar un carácter de nueva línea después de cada byte, pero eso no afecta el tiempo general ya que funciona en una CPU diferente en mi caso (agregar la -bopción hace que sea un poco más eficiente, dd cbs=1 conv=unblockparece una mejor alternativa).

Puede eliminar head -c1Gy afeitarse unos segundos estableciendo un límite en el tamaño del archivo ( limit filesize 1024mcon zsho ulimit -f "$((1024*1024))"con la mayoría de los otros shells (incluidos zsh)) en su lugar en un subshell.

Eso podría mejorarse si extrajéramos 2 dígitos para cada byte, pero necesitaríamos un enfoque diferente para eso. Lo anterior es muy eficiente porque trsolo busca cada byte en una matriz de 256 bytes. No puede hacer eso por 2 bytes a la vez, y usar cosas como hexdump -e '1/1 "%02u"'esas calcula la representación de texto de un byte usando algoritmos más complejos sería más costoso que la generación de números aleatorios en sí. Aún así, si, como en mi caso, tiene núcleos de CPU cuyo tiempo de sobra, aún puede reducir algunos segundos:

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

Obtengo (tenga en cuenta que aquí hay 1,000,000,000 de bytes en lugar de 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

Más tiempo de CPU en general, pero mejor distribuido entre mis 4 núcleos de CPU, por lo que termina tomando menos tiempo de reloj de pared. El cuello de botella es ahora hexdump.

Si utilizamos en ddlugar de la línea fold, podemos reducir la cantidad de trabajo que hexdumphay que hacer y mejorar el equilibrio de trabajo entre las 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')" -

(aquí suponiendo GNU ddpara su iflag=fullblocky status=none) que da:

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

Volver a la generación de números aleatorios es el cuello de botella.

Ahora, como señaló @OleTange, si tiene la opensslutilidad, puede usarla para obtener un generador de bytes pseudoaleatorio más rápido (especialmente en los procesadores que tienen instrucciones AES).

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

en mi sistema arroja 15 veces más bytes por segundo que /dev/urandom. (No puedo comentar cómo se compara en términos de fuente de aleatoriedad criptográficamente segura si eso se aplica a su caso de 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')" -

Ahora da:

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

vuelve a hexdumpser el cuello de botella.

Como todavía tengo CPU de sobra, puedo ejecutar 3 de ellas hexdumpen paralelo.

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

( <&3se necesita para shells que no sean los zshcomandos de cierre 'stdin on / dev / null cuando se ejecuta en segundo plano).

Ahora bajó a 6.2 segundos y mis CPU se utilizaron casi por completo.


3
Acabo de eliminar mi respuesta anterior y voté por esta. No obtuve algunos de los requisitos. Buena respuesta por cierto.
Marcelo

3
¿Por qué no genera varios dígitos en cada pasada? Incluso si lees byte por byte, aún puedes producir 2 dígitos cada vez
phuclv

@ LưuVĩnhPhúc, he eliminado la perlvariante que era significativamente más lenta de todos modos. No puedo obtener 2 dígitos por byte con ese enfoque tr | fold | paste.
Stéphane Chazelas

Estoy afk o lo probaría yo mismo, pero podría intentar convertir 42 bytes a la vez a 100-102 dígitos usando bc(luego suelte los 0, 1 o 2 dígitos más significativos).
Eric Towers

gitlab.com/ole.tange/tangetools/tree/master/rand genera 1-4 GB de pseudoaleatorio por segundo (calidad AES).
Ole Tange

23

Si tiene shufdisponible (los recientes GNU coreutils lo hacen) puede hacer esto:

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

En mi VM, esto ahora es un poco más lento que la respuesta de Stéphane en un factor de 3: 4.


shufen mi empresa, la PC no tiene -r, fmtno tiene -gtambién
phuclv

2
@ LưuVĩnhPhúc Yep - YMMV. He encontrado que la versión 8.25 de core-utils tiene estos pero 8.4 no. Qué versión estás usando?
Trauma digital

1
Estoy usando coreutils 8.13
phuclv

@ StéphaneChazelas inteligente paste/ printftruco - gracias. Su respuesta ahora es aparentemente más rápida.
Trauma digital

17

Si no necesita una aleatoriedad de muy alta calidad, y una distribución casi uniforme es lo suficientemente buena, puede ir muy rápido, especialmente en una CPU moderna con vectores enteros SIMD eficientes como x86 con SSE2 o AVX2.

Esto es como la respuesta de @ NominalAnimal ya que ambos teníamos la misma idea, pero vectorizados manualmente para x86. (Y con números aleatorios de peor calidad, pero probablemente lo suficientemente buenos para muchos casos de uso). Esto se ejecuta aproximadamente 15 o 30 veces más rápido que el código de @ Nominal, a ~ 13 GB / s de salida ASCII en un Intel Haswell de 2.5 GHz CPU con AVX2. Eso es aún menos que el ancho de banda máximo teórico de la memoria principal (DDR3-1600 de doble canal es de aproximadamente 25.6GB / s), pero estaba cronometrando la escritura en / dev / null, por lo que en realidad solo está reescribiendo un búfer que permanece caliente en la caché. Skylake debería ejecutar este mismo código significativamente más rápido que Haswell (consulte la parte inferior de esta respuesta).

Suponiendo que realmente cuente con un cuello de botella en E / S en el disco o conecte esto en alguna parte, una implementación rápida significa que su CPU ni siquiera tiene que registrar un reloj más alto que inactivo. Utiliza mucha menos energía total para producir el resultado. (Duración de la batería / calor / calentamiento global).

Esto es tan rápido que probablemente no desee escribirlo en el disco. Simplemente vuelva a generar según sea necesario (de la misma semilla si desea los mismos datos nuevamente). Incluso si desea alimentarlo a un proceso de subprocesos múltiples que puede usar todas las CPU, ejecutar esto para canalizar los datos lo dejará caliente en el caché L3 (y el caché L2 en el núcleo que lo escribió), y lo usará muy Poco tiempo de CPU. (Pero tenga en cuenta que las tuberías agregan una gran cantidad de gastos generales en lugar de escribir en /dev/null. En un Skylake i7-6700k, las tuberías wc -cu otro programa que solo lee + descarta su entrada, es aproximadamente 8 veces más lento que escribir en/dev/null , y solo usa el 70% de un CPU. Pero eso sigue siendo 4.0GB / s en una CPU de 3.9GHz.

Regenerarlo es más rápido que volver a leerlo incluso desde un SSD rápido conectado a PCIe, pero IDK si es más eficiente en energía (el multiplicador de enteros vectoriales se mantiene bastante ocupado, y probablemente consuma mucha energía, junto con otros AVX2 256 ALU de vector). OTOH, no sé cuánto tiempo de CPU tomaría la lectura del disco de algo que estaba maximizando todos los núcleos que procesan esta entrada. Supongo que un cambio de contexto para volver a generar en trozos de 128k podría ser competitivo con la ejecución de código de sistema de archivos / caché de página y la asignación de páginas para leer datos del disco. Por supuesto, si ya está caliente en el caché de página, es básicamente memcpy. ¡OTOH, ya escribimos sobre tan rápido como memcpy! (que tiene que dividir el ancho de banda de la memoria principal entre lectura y escritura). (También tenga en cuenta que escribir en la memoria que 'rep movsb(memcpy y memset optimizados en microcódigo, lo que evita RFO, desde la implementación de Andy Glew en P6 (Pentium Pro) )).


Hasta ahora, esto es solo una prueba de concepto, y el manejo de la nueva línea es aproximadamente correcto. Está mal alrededor de los extremos de un búfer de potencia de 2. Con más tiempo de desarrollo. Estoy seguro de que podría encontrar una forma más eficiente de insertar nuevas líneas que también sea exactamente correcta, con una sobrecarga al menos tan baja como esta (en comparación con la salida de solo espacios). Creo que esto es algo así como del 10 al 20%. Solo estoy interesado en saber qué tan rápido podríamos hacer que esto funcione, no en tener una versión pulida, por lo que dejaré esa parte como un ejercicio para el lector, con comentarios que describen algunas ideas.


En un Haswell i5 con su turbo máximo de 2.5GHz, con DDR3-1600MHz RAM , cronometrado produciendo 100GiB pero reducido. (Programado en cygwin64 en Win10 con gcc5.4 -O3 -march=native, omitido -funroll-loopsya que estaba teniendo dificultades para obtener tiempos decentes en esta computadora portátil prestada. Debería haber arrancado Linux en un USB).

escribiendo a / dev / null a menos que se especifique lo contrario.

  • James Hollis's: (no probado)
  • Versión de escritura de Nominal: ~ 2.21s
  • esto (SSE2): ~ 0.142s (tiempos sin escala = real = 14.232s, usuario = 13.999s, sys = 0.187s).
  • esto (AVX-128): ~ 0.140s
  • esto (AVX2): ~ 0.073s (sin escala : real = 0m7.291s, usuario = 0m7.125s, sys = 0m0.155s).
  • Esta tubería de wc -cCygwin (AVX2) , con un tamaño de búfer de 128 kB: 0,32 s con CPU a 2,38 GHz (turbo máximo de doble núcleo). (tiempos sin escala: real = 32.466s usuario = 11.468s sys = 41.092s, incluidos ambos y wc). Sin embargo, solo la mitad de los datos se copiaron realmente, porque mi tonto programa supone que la escritura hace el búfer completo, aunque ese no es el caso y cygwin write () solo hace 64k por llamada en una tubería.

Entonces, con SSE2, esto es aproximadamente 15 veces más rápido que el código escalar de @Nominal Animal. Con AVX2, es aproximadamente 30 veces más rápido. No probé una versión del código de Nominal que solo usa en write()lugar de fwrite(), pero presumiblemente para buffers grandes, stdio generalmente se mantiene fuera del camino. Si está copiando los datos, eso explicaría mucha desaceleración.


Tiempos para producir 1GB de datos en un Core2Duo E6600 (Merom 2.4GHz, 32kiB privado L1, 4MiB cachés L2 compartidos), DDR2-533MHz en Linux 4.2 de 64 bits (Ubuntu 15.10). Aún utilizando un tamaño de búfer de 128 kB para write (), no he explorado esa dimensión.

escribiendo a / dev / null a menos que se especifique lo contrario.

  • (SSE2) esto con manejo de nueva línea y 4 vectores de dígitos de cada vector de bytes aleatorios: 0.183s (cronometrado haciendo 100GiB en 18.3s, pero resultados similares para ejecuciones de 1GiB). 1.85 instrucciones por ciclo.
  • (SSE2) esto, canalizando a wc -c: 0.593s (sin escala : real = 59.266s usuario = 20.148s sys = 1m6.548s, incluido el tiempo de CPU de wc). El mismo número de llamadas al sistema write () que con cygwin, pero en realidad canaliza todos los datos porque Linux maneja los 128k de write () en una tubería.
  • NominalAnimal de fwrite()versión (gcc5.2 -O3 -march=native), ejecuta con ./decdig 100 $((1024*1024*1024/200)) > /dev/null: 3.19s +/- 0,1%, con 1,40 instrucción por ciclo. -funroll-loops hizo tal vez una pequeña diferencia. clang-3.8 -O3 -march=native: 3.42s +/- 0.1%
  • fwriteTubería nominal a wc -c: real = 3.980s usuario = 3.176s sys = 2.080s
  • Versión de línea a tiempo de James Hollis ( clang++-3.8 -O3 -march=native): 22.885s +/- 0.07%, con 0.84 instrucciones por ciclo. (g ++ 5.2 fue ligeramente más lento: 22.98s). Escribir solo una línea a la vez probablemente duele significativamente.
  • Stéphane Chazelas's tr < /dev/urandom | ...: real = 41.430s usuario = 26.832s sys = 40.120s. trestaba obteniendo todo el núcleo de la CPU para sí mismo la mayor parte del tiempo, pasando casi todo su tiempo en el controlador del núcleo generando bytes aleatorios y copiándolos en una tubería. El otro núcleo en esta máquina de doble núcleo estaba ejecutando el resto de la tubería.
  • time LC_ALL=C head -c512M </dev/urandom >/dev/null: es decir, solo leer tanta aleatoriedad sin tuberías: real = 35.018s usuario = 0.036s sys = 34.940s.
  • Programa perl de Lưu Vĩnh Phúc (perl v5.20.2 de Ubuntu15.10)
    LANG=en_CA.UTF-8:: real = 4m32.634s usuario = 4m3.288s sys = 0m29.364.
    LC_ALL=C LANG=C: real = 4m18.637s usuario = 3m50.324s sys = 0m29.356s. Aún muy lento.

  • (SSE2) esto sin manejo de línea nueva , y 3 o 4 vectores de dígitos de cada vector de bytes aleatorios (casi exactamente la misma velocidad: el dig3 = v%10paso es sobre el punto de equilibrio en este HW): 0.166s (1.82 instrucciones por ciclo) . Este es básicamente el límite inferior para lo que podemos acercarnos con un manejo de nueva línea perfectamente eficiente.

  • (SSE2) Versión anterior de esto sin manejo de línea nueva, pero solo obteniendo un dígito por elemento uint16_t usando v%10, 0.222 segundos +/- 0.4%, 2.12 instrucciones por ciclo. (Compilado con gcc5.2,. -march=native -O3 -funroll-loopsDesenrollar bucles sí ayuda para este código en este hardware. No lo use a ciegas, especialmente para programas grandes).
  • (SSE2) Versión anterior de esto, escribiendo en un archivo (en un RAID10f2 de 3 discos duros magnéticos rápidos, no muy optimizado para escrituras): ~ 4 segundos. Podría ir más rápido ajustando la configuración del búfer de E / S del kernel para permitir una mayor cantidad de datos sucios antes de los bloques write (). El tiempo del "sistema" sigue siendo ~ 1.0 segundos, mucho más alto que el tiempo del "usuario". En este viejo sistema con RAM DDR2-533 lenta, el núcleo tarda ~ 4 veces más en memorizar los datos en el caché de página y ejecutar funciones XFS que en mi bucle para seguir reescribiéndolo en su lugar en un búfer que permanece caliente cache.

Cómo está hecho

Un PRNG rápido es obviamente esencial. xorshift128 + se puede vectorizar, por lo que tiene dos o cuatro generadores de 64 bits en paralelo, en elementos de un vector SIMD. Cada paso produce un vector completo de bytes aleatorios. ( Implementación de 256b AVX2 aquí con intrínsecos Intel ). Lo elegí por la elección de xorshift * de Nominal, porque la multiplicación de enteros vectoriales de 64 bits solo es posible en SSE2 / AVX2 con técnicas de precisión extendida .


Dado un vector de bytes aleatorios, podemos dividir cada elemento de 16 bits en múltiples dígitos decimales. Producimos múltiples vectores de elementos de 16 bits que son cada uno un dígito ASCII + espacio ASCII . Lo almacenamos directamente en nuestro búfer de salida.

Mi versión original solo solía x / 6554obtener un dígito aleatorio de cada elemento uint16_t de un vector. Siempre está entre 0 y 9, inclusive. Está sesgado 9, porque (2^16 -1 ) / 6554es solo 9.99923. (6554 = ceil ((2 ^ 16-1) / 10), lo que garantiza que el cociente sea siempre <10.)

x/6554se puede calcular con una multiplicación por una constante "mágica" ( el recíproco de punto fijo ) y un desplazamiento a la derecha del resultado de la mitad superior. Este es el mejor caso para la división por una constante; algunos divisores realizan más operaciones, y la división firmada requiere trabajo adicional. x % 10tiene un sesgo similar y no es tan barato de calcular. (la salida asm de gcc es equivalente a x - 10*(x/10), es decir, una multiplicación y resta adicionales en la parte superior de la división usando un inverso multiplicativo modular.) Además, el bit más bajo de xorshift128 + no es de alta calidad , por lo que es mejor dividir para tomar entropía de bits altos ( para calidad y velocidad) que el módulo para tomar entropía de bits bajos.

Sin embargo, podemos usar más de la entropía en cada uint16_t mirando los dígitos decimales bajos, como la digit()función de @ Nominal . Para obtener el máximo rendimiento, decidí tomar los 3 dígitos decimales bajos y x/6554, para guardar un PMULLW y PSUBW (y probablemente algunos MOVDQA) frente a la opción de mayor calidad de tomar los 4 dígitos decimales bajos. x / 6554 se ve ligeramente afectado por los 3 dígitos decimales bajos, por lo que existe cierta correlación entre los dígitos del mismo elemento (separación de 8 o 16 dígitos en la salida ASCII, dependiendo del ancho del vector).

Creo que gcc se divide por 100 y por 1000, en lugar de una cadena más larga que se divide sucesivamente por 10, por lo que probablemente no esté acortando significativamente la longitud de la cadena de dependencia no transportada en bucle que produce 4 resultados de cada salida PRNG. port0 (multiplicación y cambio de vectores) es el cuello de botella debido a las inversas multiplicativas modulares y los cambios en xorshift +, por lo que definitivamente es útil guardar una multiplicación de vectores.

xorshift + es tan rápido que incluso usar solo ~ 3.3 bits de aleatoriedad de cada 16 (es decir, 20% de eficiencia) no es mucho más lento que dividirlo en varios dígitos decimales. Solo aproximamos la distribución uniforme, porque esta respuesta se centra en la velocidad siempre que la calidad no sea tan mala.

Cualquier tipo de comportamiento condicional que mantenga un número variable de elementos requeriría mucho más trabajo. (Pero tal vez podría hacerse de manera algo eficiente usando las técnicas de empaquetado a la izquierda SIMD . Sin embargo, eso se vuelve menos eficiente para tamaños de elementos pequeños; las tablas de búsqueda de máscara aleatoria gigante no son viables, y no hay una mezcla aleatoria de cruce de carriles AVX2 con menos de 32- elementos de bit. Una versión de 128b PSHUFB aún podría generar una máscara sobre la marcha con BMI2 PEXT / PDEP, como puede hacerlo con AVX2 con elementos más grandes , pero es complicado porque un entero de 64 bits solo contiene 8 bytes. en esa respuesta tiene algún código que podría funcionar para conteos de elementos más altos).


Si la latencia del RNG es un cuello de botella, podríamos ir aún más rápido ejecutando dos vectores de generadores en paralelo, alternando cuál usamos. El compilador aún puede mantener fácilmente todo en registros en un bucle desenrollado, y eso permite que las dos cadenas de dependencia se ejecuten en paralelo.

En la versión actual, cortando la salida del PRNG, realmente tenemos un cuello de botella en el rendimiento del puerto 0, no en la latencia PRNG, por lo que no hay necesidad de eso.


El código: versión AVX2

Versión completa con más comentarios sobre el explorador del compilador Godbolt .

No muy ordenado, lo siento, tengo que dormir y quiero publicar esto.

Para obtener la versión SSE2, s/_mm256/_mm, s/256/128/, s/v16u/v8u/, y el cambio vector_size(32)a 16. También cambiar el incremento de nueva línea de 4 x 16 a 4 * 8. (Como dije, el código es desordenado y no está bien configurado para compilar dos versiones. Originalmente no planeaba hacer una versión AVX2, pero realmente quería probar en una CPU Haswell a la que tenía acceso).

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

}

Compile con gcc, clang o ICC (o con suerte cualquier otro compilador que comprenda el dialecto GNU C de C99 y los intrínsecos de Intel). Las extensiones de vector GNU C son muy convenientes para que el compilador genere los números mágicos para la división / módulo utilizando inversos multiplicativos modulares, y ocasionalmente __attribute__s son útiles.

Esto podría escribirse de forma portátil, pero tomaría más código.


Notas de rendimiento:

La tienda superpuesta para insertar nuevas líneas tiene una sobrecarga considerable para decidir dónde colocarla (predicciones erróneas de sucursales y cuellos de botella frontend en Core2), pero la tienda en sí no tiene ningún impacto en el rendimiento. Al comentar solo esa instrucción de tienda en el compilador del compilador (dejando todas las ramificaciones iguales), el rendimiento en Core2 no cambió por completo, y las ejecuciones repetidas dieron el mismo tiempo a +/- menos del 1%. Así que concluyo que el búfer / caché de la tienda lo maneja bien.

Aún así, usar algún tipo de ventana giratoria ascii_digitspacecon un elemento que tenga una nueva línea podría ser aún más rápido, si desenrollamos lo suficiente como para que desaparezcan los contadores / ramificaciones.


Escribir en / dev / null es básicamente un no-op, por lo que el búfer probablemente se mantiene caliente en la caché L2 (256 kB por núcleo en Haswell). Se espera una aceleración perfecta de 128b a 256b: no hay instrucciones adicionales y todo (incluidas las tiendas) ocurre con el doble de ancho. Sin embargo, la rama de inserción de nueva línea se toma el doble de veces. Desafortunadamente, no tuve tiempo en mi configuración de Haswell cygwin con esa parte #ifdefeditada.

2.5GHz * 32B / 13.7GB / s = 5.84 ciclos por AVX2-store en Haswell. Eso es bastante bueno, pero podría ser más rápido. Quizás haya algo de sobrecarga en las llamadas al sistema cygwin de lo que pensaba. No intenté comentarlos en la salida asm del compilador (lo que garantizaría que nada se optimice).

La memoria caché L1 puede mantener una tienda de 32B por reloj, y L2 no tiene un ancho de banda mucho menor (sin embargo, una latencia más alta).

Cuando miré a IACA hace algunas versiones (sin la ramificación de las nuevas líneas, pero solo obtenía un vector ASCII por vector RNG), estaba prediciendo algo así como una tienda de vectores 32B por 4 o 5 relojes.

Esperaba obtener una mayor velocidad al extraer más datos de cada resultado de RNG, basándome en mirar el asm, considerando las guías de Agner Fog y otros recursos de optimización para los que he agregado enlaces en el wiki de etiquetas SO x86 .

Probablemente sería significativamente más rápido en Skylake , donde la multiplicación y el desplazamiento de enteros vectoriales pueden ejecutarse en el doble de puertos (p0 / p1) en comparación con Haswell (solo p0). xorshift y la extracción de dígitos utilizan muchos cambios y multiplicaciones. ( Actualización: Skylake lo ejecuta a 3.02 IPC, dándonos 3.77 ciclos por tienda AVX2 de 32 bytes , cronometrado a 0.030s por iteración de 1GB, escribiendo /dev/nullen Linux 4.15 en i7-6700k a 3.9GHz.


No requiere el modo de 64 bits para funcionar bien . La versión SSE2 es igual de rápida cuando se compila -m32, porque no necesita muchos registros de vectores, y toda la matemática de 64 bits se realiza en vectores, no en registros de propósito general.

En realidad, es un poco más rápido en modo de 32 bits en Core2, porque la macro fusión de comparación / ramificación solo funciona en modo de 32 bits, por lo que hay menos uops para el núcleo fuera de orden (18.3s (1.85 instrucciones por reloj) vs 16.9s (2.0 IPC)). El tamaño de código más pequeño por no tener prefijos REX también ayuda a los decodificadores de Core2.

Además, algunos movimientos de vector reg-reg se reemplazan con cargas, ya que ya no se fijan todas las constantes en los registros vectoriales. Dado que el rendimiento de carga del caché L1 no es un cuello de botella, esto realmente ayuda. (por ejemplo, multiplicar por un vector constante de set1(10): movdqa xmm0, xmm10/ se pmullw xmm0, xmm1convierte en movdqa xmm0, [constant]/ pmullw xmm0, xmm1.) Dado que MOVDQA reg-reg requiere un puerto ALU, compite con el trabajo real que se realiza, pero una carga MOVDQA solo compite por el ancho de banda de decodificación front-end. (Tener una dirección de 4 bytes dentro de muchas instrucciones cancela gran parte de la ganancia al guardar los prefijos REX.

No me sorprendería si salvando ALU MOVDQA uops es de donde provienen las ganancias reales, ya que la interfaz debería mantenerse bastante bien con el promedio de 2.0 IPC.

Todas estas diferencias desaparecen en Haswell, donde todo debería ejecutarse desde la memoria caché decodificada-uop, si no el búfer de bucle invertido. La macro fusión de rama ALU + funciona en ambos modos desde Nehalem.


66
¡Me encanta cómo te metiste en el "modo bestia" en el tema! :) Más importante aún, es un excelente ejemplo de qué tipo de ganancias están disponibles, si realmente necesita o desea obtener el máximo rendimiento, utilizando conocimientos de muy bajo nivel del hardware en cuestión. Además, solo estamos usando un hilo aquí; La mayoría de los procesadores Intel / AMD de escritorio y servidor actuales (e incluso los ARM en tabletas ligeras y SBC) tienen múltiples núcleos, por lo que todavía hay más aceleraciones disponibles en el mundo real. Y, por último, cuán poco prácticas son las preguntas de "la forma más rápida de hacerlo" , debido al gran esfuerzo involucrado.
Nominal Animal

1
@NominalAnimal: Sí, incluso un núcleo ARM quad o octo lento podría saturar fácilmente el ancho de banda de la memoria principal haciendo lo mismo con NEON (incluso si estuvieran conectados al DDR3 de doble canal rápido), si tiene un número entero de 64 bits, SIMD agrega y cambia . Supongo que NEON tiene multiplicaciones de elementos de 16 bits para el trabajo de audio. La programación de instrucciones sería mucho más trabajo para un ARM en orden, porque cada iteración de la cadena de dependencia transportada en bucle (el xorshift128 +) alimenta algunas cadenas de dependencia independientes de cortar eso y almacenarlo en la memoria ...
Peter Cordes

... La ejecución fuera de orden lo come para el desayuno, porque todo es lo suficientemente corto como para que varias iteraciones quepan en el ROB (192 uops en HSW IIRC). (es decir, la "ventana" de instrucciones que ve la ejecución fuera de orden incluye múltiples iteraciones). Por lo tanto, la CPU puede estar terminando la tienda final hace 2 o 3 iteraciones mientras se inicia al comienzo de la iteración actual. Esto oculta la latencia de las cadenas independientes, por lo que solo importa el rendimiento. En un núcleo en orden, esto requeriría una canalización de software ...
Peter Cordes

... Un buen compilador ARM debería hacer algo de eso por usted si lo escribe con intrínsecos (o sintaxis de vectores nativos GNU C para todo, como debería haber hecho en primer lugar). No tengo ninguna experiencia en hacer eso de verdad, por lo que es posible que necesite masajear su bucle y tal vez hacer un desenrollado manual / canalización de software en la fuente para obtener una buena solución. (Hay algunos núcleos ARM fuera de servicio, que se encuentran en los teléfonos de gama alta, pero no tienen una ventana fuera de servicio tan grande como Haswell. OTOH, también tienen un rendimiento pico más bajo, por lo que hay menos para ganar encontrando más ILP).
Peter Cordes

1
@NominalAnimal: también, de acuerdo en la tontería de la pregunta. "Más rápido" sin restricciones en la calidad de la aleatoriedad es una tontería ... Con BTRFS, los mismos datos en el disco pueden ser parte de un archivo varias veces (ver EXTENT_SAME en 4.2 ). Por lo tanto, puede generar un 4kiB o 1MB aleatorio y repetirlo. Es aleatoriedad con un período corto, pero sigue siendo aleatorio y solo cuesta E / S de metadatos. (En realidad, necesita que el bloque termine con una nueva línea. Mcm (4096, 4096 * 200) = 4096 * 200 = 819200 = 800kiB, por lo que su bloque de repetición es cualquier múltiplo de eso.)
Peter Cordes

14

Aquí hay una solución que espero que sea fácil de entender:

od -An -x /dev/urandom | tr -dc 0-9 | fold -w100 | awk NF=NF FS= | head -c1G
  • odcrea una secuencia uniforme de dígitos hexadecimales de /dev/random.
  • trse deshace de las letras, solo mantiene los 0-9dígitos
  • fold asegura que haya 100 dígitos por línea
  • awk inserta espacios dentro de las líneas
  • head trunca la entrada a 1 gigabyte

2
Esa es una buena forma alternativa de producir más de un dígito por byte de / dev / random mientras todavía tiene una distribución uniforme, que produce 320 dígitos por cada 256 bytes de / dev / urandom en promedio (menos que cuando convierte bytes <200 módulo 100 a decimal que le da 400 dígitos por cada 256 bytes sin embargo).
Stéphane Chazelas

6

Puede usar el jotcomando para esto:

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

1
@DigitalTrauma Mi versión de fmtno tiene una opción de ancho de objetivo. De todos modos, ¡será exacto porque todos los dígitos ocupan exactamente una columna!
cabeza de jardín

Para el registro, mi fmtversión es fmt (GNU coreutils) 8.25(Ubuntu 16.04)
Digital Trauma

2
el número correcto para medio gb es: 1024 * 1024 * 1024/2 =536870912
Olivier Dulac

1
@OlivierDulac Depende de qué "gigabyte" esté hablando. Algunas personas usan 1 Gb para significar 10 ^ 9 en lugar de 2 ^ 30, a pesar de que es técnicamente incorrecto. Además, me gustan los buenos números redondos :)
gardenhead

66
@gardenhead, cada vez más personas tienden a pasar a Gigabyte == 1e9 y Gibibyte == 2 ^ 30, ya que esa es la definición estándar de IEC. Ver Wikipedia . Tenga en cuenta que en sí Gb sería más bien Giga bits .
Stéphane Chazelas

6

Esto es similar al método de Stéphane Chazelas, sin embargo, leí 64 bits a la vez para mejorar el rendimiento. La distribución sigue siendo uniforme, pero ahora obtienes 19 dígitos por cada 8 bytes en lugar de solo 8 en el mejor caso como antes

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

En la plataforma de 32 bits, se leerán 9 dígitos cada vez en lugar de 19.


Esto puede generar una excepción si su sistema no admite números enteros de 64 bits o perlno está compilado con soporte cuádruple.
Cuonglm

@cuonglm sí, como dije si perl no tiene 64 bits en ese sistema, entonces el programa debe cambiarse next if $n >= 1000000000; $s = sprintf("%09u", $n);para obtener solo 9 dígitos
phuclv

No puede, el programa se bloqueará $n = unpack("Q")si quad no es compatible.
Cuonglm

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

1
Lo sentimos, pero esto obtiene 19 dígitos de la entrada de 8 bytes solo aproximadamente el 54.2% del tiempo y ninguno el resto, con un promedio de 1.29 dígitos por byte de entrada. Si usa Stephane más <16e18y divide entre 16, obtiene 18 dígitos 86.7% para 1.95 dpB. Con 32 bits, <4e9 /4obtiene 9 dígitos 93.1% para 2.10 dpB. Pero 5 bytes (como hexadecimal (H10)) <1e12da 12 dígitos 90.9% para 2.18 dpB, o dividir el hexadecimal por la mitad y hacer cada mitad <1e6 da 6 dígitos 95.4% para 2.29 dpB; esto se acerca al límite de log_10 (256) = 2.41.
dave_thompson_085

3

Estoy de acuerdo con Nominal Animal en el uso de un lenguaje de programación compilado si necesita velocidad. Sin embargo, no tiene que escribir su propio código RNG en C. C ++ 11 ofrece el excelente Mersenne Twister como parte de su biblioteca estándar.

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

El código anterior es razonablemente simple y toma aproximadamente un minuto cuando canalizo la salida a un archivo. Podemos ir mucho más rápido creando una cadena lo suficientemente grande para 100 dígitos y hackeando los dígitos. Esto nos permite llamar a cout cada línea en lugar de cada dígito.

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

Este código toma mi máquina alrededor de seis segundos. Recuerde que es una salida estándar, así que canalícela a un archivo.

Tengo un par de renuncias. Primero, estoy escribiendo esto en una PC con Windows. Creo que todas las bibliotecas están presentes en Linux, pero si me equivoco, asegúrese de señalarlo.

Además, en realidad genera exactamente 500 millones de dígitos separados por espacios, que técnicamente es un gigabyte, pero tal vez no sea exactamente lo que deseaba. Produce 5 millones de líneas, 100 dígitos por línea. Si la diferencia es importante, puede aumentar el número de líneas. En mi cuadro de Windows, el archivo parece ser un poco más grande que 10 ^ 9 bytes, lo que creo que tiene algo que ver con los caracteres adicionales de nueva línea.


2
¡Hey, la crítica no es realmente justa! :) La mayor parte de mi programa es el análisis de parámetros de línea de comandos. Si también omito comentarios, verificaciones de errores y codifico el número de columnas y líneas de salida, puedo hacer que sea menos del doble del tamaño de su código, apenas monstruoso . :) Bromas aparte: Sí, las bibliotecas están disponibles en la mayoría de las distribuciones de Linux. En mi computadora portátil, su línea a la vez toma aproximadamente 14 segundos, mientras que mi versión de línea a la vez toma solo 1.3 segundos. La diferencia solo se debe al PRNG: Mersenne Twister es mucho más lenta que Xorshift64 *.
Nominal Animal

1
Hay una cosa práctica que me gustaría señalar que te has perdido, pero espero que no lo tomes como negatividad, solo algo en lo que pensar: como mencioné en mi respuesta, los programas únicos rara vez valen la pena. tiempo que tardaron en escribir. Es por eso que casi siempre vale la pena agregar el análisis de línea de comandos y el texto de ayuda. Tengo un gran conjunto de tales programas de utilidad, y en lugar de buscar sus fuentes para averiguar qué hace cada uno de ellos, simplemente los ejecuto, para que me lo digan; y puedo modificar su comportamiento lo suficiente como para satisfacer más de una necesidad. Amortizando el costo de desarrollo.
Nominal Animal

@NominalAnimal otra cosa importante es que canalizó la salida a lo /dev/nullque sería mucho más rápido que escribir en un archivo real
phuclv

@ LưuVĩnhPhúc: Bueno, en realidad no. Este lappy tiene un SSD Samsung de 128 GB, con ~ 500 MB / s de lectura y escritura secuenciales. Ponga cuatro en una configuración de software RAID0 de Linux, y obtendrá más de un gigabyte de lecturas y escrituras por segundo cuando genere conjuntos de datos tan grandes (estimo ~ 1.75 TB / s). 1 GB / s se alcanzó hace años con 12 unidades SATA (platos giratorios, ni siquiera SSD) con Linux sw-RAID0. (Nota: bytes / s, no bits / s.) Claro, suena tonto para una máquina "normal", pero aquellos que juegan con grandes conjuntos de datos, encuentran que vale la pena: ahorras tiempo de todo lo que haces (con grandes conjuntos de datos) de esa manera.
Nominal Animal

1
@NominalAnimal y Lu'u: lo que es más importante, si tiene suficiente RAM, el programa puede salir mucho antes de que todos los datos estén en el disco. La mayor parte del trabajo en una write()llamada de sistema grande es una memoria en el caché de página, que se bloquea solo si el núcleo decide hacer eso en lugar de asignar más espacio de búfer. Este programa solo debe tener un cuello de botella en la E / S del disco cuando la memoria es escasa, o si ha utilizado O_DIRECT para omitir el caché de página. Si write()en trozos más pequeños que el tamaño de caché, es de esperar que sus datos solo vayan a la memoria principal una vez, y el búfer que se reescribe en su lugar permanece caliente en el caché L2 o L3.
Peter Cordes

1

Depende de su definición de "aleatorio". Si te refieres a criptográficamente al azar, solo tienes que conseguir una buena biblioteca y morder la bala, esperar a que se ejecute.

Si solo necesita algo que parezca bastante aleatorio, esta es una manera fácil:

  1. Obtenga un archivo de varios Gb de largo. Tu película favorita será buena.
  2. Gzip, una manera fácil de exprimir patrones repetidos
  3. Ir a través del archivo un nybble (medio byte) a la vez. Cada valor estará entre 0 y 15. Deseche menos de 1 o más de 10. Reste 1 de cada uno de los primeros mil millones de sobrevivientes y escríbalo como un dígito.

Puede tardar una hora en ejecutarse en una máquina lenta; lo suficientemente rápido y aleatorio para la mayoría de los propósitos.


99
/dev/urandomes probable que sea mejor que gzip, tanto en velocidad como en aleatoriedad.
Stig Hemmer

Get a file that is several Gb longnecesitará un archivo ** de al menos 8 Gb` para obtener un archivo de 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
Bienvenido al sitio! Ver enlaces en mi página de perfil. Hay muchos problemas aquí que veo casi universalmente en los scripts de shell, pero eso no los hace correctos.
Comodín el

2
@Wildcard: nunca cat file | trcuando puedes simplemente tr <file. IIRC, puedes incluso <file tr. Pensé que solo estabas hablando de que este script de shell se ve torpe y lento, como du | awkdespués de cada línea para verificar el tamaño, y volver a abrir el archivo para agregar cada línea en lugar de redirigir fuera del bucle.
Peter Cordes

2
@PeterCordes, sí. ¿Por qué usar un bucle de shell para procesar texto se considera una mala práctica? es particularmente relevante: este script se basa en la idea de que Bash es un lenguaje de programación como C, que no lo es. Pero, \ @NamNT, espero que te quedes en este sitio porque está claro que tienes una mente muy lógica. :)
Comodín

44
@PeterCordes cat /dev/urandom | busy-cmdes uno de esos casos raros en los que puede tener sentido, ya que puede dividir la generación aleatoria y el cmd ocupado entre procesadores. No tanto para tr, pero hace una diferencia para Sam, odpor ejemplo.
Stéphane Chazelas

1
@ StéphaneChazelas: ¡oh, cierto! Sí, la llamada al sistema read () es donde se pasa el tiempo de CPU RNG.
Peter Cordes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.