Який найшвидший спосіб створити текстовий файл розміром 1 Гб, що містить випадкові цифри?


52

Я спробував скрипт bash, але для створення простого файлу розміром 1 Мб знадобилося занадто багато часу. Я думаю, що відповідь полягає у використанні /dev/randomабо /dev/urandom, але інші публікації тут лише показують, як додати всі види даних у файл за допомогою цих речей, але я хочу додати лише числа.

Отже, чи є команда, яку я можу використовувати для створення випадкового файлу розміром 1 ГБ, що містить лише числа від 0 до 9?

Редагувати: Я хочу, щоб результат був приблизно таким

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

Діапазон становить 0 - 9, тобто лише цифри 0, 1, 2, 3, 4, 5, 6, 7, 8 і 9. Також мені потрібно, щоб вони були пробілами і 100 на рядок, до nкількості рядків. Мені це не байдуже, я хочу, щоб мій кінцевий розмір був 1 Гб.

Редагувати: я використовую Ubuntu 16.04 LTS



21
Напевно, ви повинні сказати, що ви маєте на увазі під "випадковим" - криптографічна сила випадковості, чи псевдовипадкова послідовність є достатньою?
Toby Speight

4
@posixKing: Зауважте, що хоча моя відповідь, безумовно, чітко - я насправді не пропоную написати програму C для такого завдання! -, якщо ви регулярно генеруєте такі величезні набори даних або ви часто їх генеруєте, підхід може заощадити ваш час. (На моєму ноутбуці він генерує 1 Гб пробілів з цифрами приблизно за десять секунд.) Однак, якщо це одноразово, навіть не думайте писати програму C для цього (якщо ви не любите програмування, і врахуйте це практика чи таке); команди команд і утиліти досягають завдання за менший загальний витрачений час та зусилля.
Номінальна тварина

7
Це досить швидко та сумісно з RFC 1149.5:yes 4 | tr '\n' ' ' | fold -w 200 | head -c1G
Меттью Крамлі

Відповіді:


38

Частково це відповідь язиком у щоках через назву питання.

Коли ви шукаєте "найшвидший шлях до ..." , відповідь майже завжди є якимсь спеціалізованим інструментом. Цей "відповідь" показує один такий інструмент, просто щоб ви могли експериментувати.

Це не є серйозною відповіддю, тому що ви не повинні шукати спеціалізовані інструменти для роботи, яку ви робите лише один раз або дуже рідко. Розумієте, ви в кінцевому підсумку витратите більше часу на пошуки інструментів і вивчення їх, ніж насправді займаючись речами. Оболонки та комунальні послуги подобаються bashі awkне найшвидші, але зазвичай ви можете написати однолінійку, щоб досягти роботи, витрачаючи лише секунди. perlТакож можна використовувати кращі мови написання , хоча крива навчання для perlних крута, і я не вагаюся рекомендувати її для таких цілей, тому що мене травмували жахливі проекти Perl. pythonз іншого боку, дещо обмежений його досить повільним введенням / виводом; це лише проблема, коли ви фільтруєте або генеруєте гігабайти даних.

У будь-якому випадку, наступна прикладна програма C89 (яка використовує POSIX.1 для тактових сигналів з більшою точністю, лише якщо вони доступні) повинна досягати швидкості генерації приблизно 100 Мб / с (тестується в Linux на ноутбуці з процесором Intel i5-4200U, підключивши вихід до /dev/null), використовуючи досить хороший генератор псевдовипадкових чисел. (Вихід повинен пройти всі тести BigCrunch, за винятком тесту MatrixRank, оскільки код використовує xorshift64 * та метод виключення, щоб уникнути зміщення цифр.)

десяткові цифри.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;
}

Ми можемо зробити це набагато швидше, якщо перейти до буфера рядків, і fwrite()це один раз замість того, щоб виводити кожну цифру за один раз. Зауважте, що ми все ще зберігаємо потік повністю буферизованим, щоб уникнути часткових записів (без потужності двох), якщо вихід є блоковим пристроєм.

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

Примітка: обидва приклади відредаговані 2016-11-18, щоб забезпечити рівномірний розподіл цифр (нуль виключено; див. Наприклад, тут для порівняння та деталей щодо різних генераторів псевдовипадкових чисел).

Компілюйте, використовуючи, наприклад,

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

і необов'язково встановлювати на всій системі для /usr/binвикористання

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

Він займає кількість цифр на рядок та кількість рядків. Тому що 1000000000 / 100 / 2 = 5000000(п’ять мільйонів; загальний байт, розділений на стовпці, розділені на 2), ви можете використовувати

./decimal-digits 100 5000000 > digits.txt

генерувати розмір гігабайт digits.txtза бажанням ОП.

Зауважте, що сама програма написана більше з читабельністю, ніж на увазі ефективністю. Моя мета тут не демонструвати ефективність коду - я все одно використовую POSIX.1 та низькорівневий введення / виведення, а не загальні інтерфейси C - але щоб ви могли легко побачити, який баланс є із витраченими зусиллями при розробці виділених інструментів на відміну від їх продуктивності порівняно з одношаровими або короткими скриптами оболонки або awk.

Використовуючи бібліотеку GNU C, виклик fputc()функції для кожного виводу символів має дуже невеликі накладні витрати (непрямий виклик функції або умовні умови - FILEінтерфейс насправді досить складний і універсальний, ви бачите). На цьому конкретному ноутбуці Intel Core i5-4200U перенаправлення виводу на /dev/nullпершу (fputc) версію займає близько 11 секунд, тоді як версія за часом - всього 1,3 секунди.

Мені трапляється часто писати такі програми та генератори лише тому, що мені подобається грати з величезними наборами даних. Я дивний таким чином. Наприклад, я одного разу написав програму для друку всіх кінцевих позитивних значень IEEE-754 з плаваючою комою у текстовий файл з достатньою точністю, щоб отримати точно таке ж значення при синтаксичному розборі. Файл мав кілька гігабайт (можливо, 4G або близько того); не так багато кінцевих позитивних floats, як можна подумати. Я використовував це для порівняння реалізацій, які читають та аналізують такі дані.

Для випадків звичайного використання, як, наприклад, у ОП, кращий підхід - сценарії оболонки та сценарії та одноклайники. Менш часу витрачається на виконання загального завдання. (За винятком випадків, коли вони потребують іншого файлу щодня або близько того, або є багато людей, яким потрібен інший файл, у якому - рідкісний - випадок, спеціалізований інструмент, як вище, може вимагати витрачених зусиль.)


Так, напевно, mmap()це найпростіший маршрут до найкращої швидкості вводу / виводу - але орієнтир, перш ніж пред'являти будь-які претензії!
Toby Speight

@TobySpeight: У Linux низький рівень вводу / виводу, тобто використання write(), як правило, швидше, ніж mmap(). fwrite()не набагато повільніше. Так, я це визначив (тільки не для цього конкретного прикладу); write()у великих фрагментах (262144, 524288 або 1048576 байт), як правило, перевершує інші методи. Версія, fputc()впроваджена в бібліотеці GNU C (яку я також широко оцінила) повільна з кількох причин; зокрема, впровадження, що має робити або умовні стрибки, або непрямі виклики для кожного доданого символу; що невеликі накладні витрати, які так часто виникають
Номінальна тварина

Просто з інтересу - ви зробили порівняння ефективності з іншими відповідями?
Цифрова травма

2
@DigitalTrauma: Я просто запустив їх для вас, перенаправляючи вихід на /dev/null. Сценарій Стефана Шазеласа займає близько 52 секунд; фрагмент perl (включаючи headфільтрування) близько 58 секунд; ваш shufфрагмент (з правильним терміном; ви вимірюєте лише час шуфу, якщо припустити, що введення пасти більше не буде) займає приблизно 69 секунд. Джеймс Холліс ' C ++ 11 програма за лінією за часом займає 14 секунд. Вищеописана програма займає 10 секунд.
Номінальна тварина

3
(Програв мій порядок думок вище, вибачте.) Точка полягає в тому, що вибір правильного алгоритму - досить випадковий, але дуже швидкий PRNG тут - призвів майже до порядку збільшення (10 ×) швидкості. (Остання версія моїх програм приблизно в 40 разів швидша за фрагменти оболонки або perl.) Це типово. Можливо, я мав би підкреслити вибір правильного алгоритму, коли писав програму більше у своїй відповіді вище? (З іншого боку, це не питання програмування, а питання Unix / Linux про те, як генерувати багато цифр.)
Nominal Animal

81

Це:

 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

(припустимо, що headреалізація підтримує -c) в моїй системі здається досить швидкою.

trпереводить весь байтовий діапазон (від 0 до 255, від 0 до 0377 в восьмеричному): 25 перших байтів як 0, 25 наступних як 1 ... потім 25 9 решта (250 до 255) на "х", які ми потім відкиньте (з tr -d x), як ми хочемо рівномірного розподілу (якщо припустити, що він /dev/urandomмає рівномірний розподіл), і тому не давати зміщення деяким цифрам.

Це дає одну цифру для 97% байтів /dev/urandom. fold -w 1робить це однією цифрою на рядок. paste -sназивається зі списком розділювачів, який складається з 99 символів пробілу та одного символу нового рядка, щоб у кожному рядку було 100 розділених пробілами цифр.

head -c1Gотримаємо перший GiB (2 30 ) цього. Зауважте, що останній рядок буде усіченим і невизначеним. Ви можете скоротити до 2 30 -1 і додати пропущений новий рядок вручну, або скоротити до 10 9 байт, а це 50 мільйонів з цих 200-байтних рядків ( head -n 50000000це також зробить його стандартною / портативною командою).

Ці таймінги (отримані за zshдопомогою чотирьохядерної системи) дають вказівку на те, де витрачається час процесора:

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

Перший tr- це горловина пляшки, більшість часу проводиться в ядрі (я думаю, для генерації випадкових чисел). Час приблизно відповідає швидкості, з якої я можу отримати байти /dev/uramdom(приблизно 19 Мбіт / с, і тут ми виробляємо 2 байти на кожен 0,97 байт / дев / урадом із швидкістю 32 Мбіт / с). foldздається, витрачає необгрунтовану кількість часу на процесор (15 секунд) просто для того, щоб вставити символ нового рядка після кожного байту, але це не впливає на загальний час, оскільки він працює в іншому процесорі в моєму випадку (додавання цього -bпараметра робить це дуже трохи більше ефективна, dd cbs=1 conv=unblockздається, краща альтернатива).

Ви можете позбутися від з head -c1Gі збрити кілька секунд, встановивши обмеження на розмір файлу ( limit filesize 1024mз zshабо ulimit -f "$((1024*1024))"з більшістю інших оболонок ( в тому числі zsh)) , а не в субоболочке.

Це можна було б покращити, якби ми дістали по 2 цифри за кожен байт, але для цього нам потрібен інший підхід. Вищезазначене є дуже ефективним, оскільки trпросто шукає кожен байт у масиві 256 байт. Це не може зробити це 2 байти одночасно, і використання таких речей, як hexdump -e '1/1 "%02u"'обчислює текстове представлення байту за допомогою більш складних алгоритмів, було б дорожче, ніж саме генерування випадкових чисел. Тим не менш, якщо як у моєму випадку, у вас є ядра центрального процесора, час для запасу яких, можливо, все-таки вдасться поголитися за кілька секунд:

З:

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

Я розумію (проте зауважте, що тут це 1 000 000 000 байт на відміну від 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

Більше загальний час процесора, але краще розподіляється між моїми 4 ядрами процесора, так що це забирає менше часу настінного годинника. Вузьке місце зараз hexdump.

Якщо ми використовуємо ddзамість рядкових fold, ми можемо фактично зменшити обсяг роботи, що hexdumpпотрібно виконати, та покращити баланс роботи між процесорами:

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

(тут припускаємо GNU ddдля свого iflag=fullblockта status=none), який дає:

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

Назад до покоління випадкових чисел, що є вузьким місцем.

Тепер, як вказував @OleTange, якщо у вас є opensslутиліта, ви можете використовувати її, щоб отримати швидший (особливо для процесорів, що мають інструкції AES) псевдовипадковий генератор байтів.

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

в моїй системі вивергає 15 разів більше байт в секунду , ніж /dev/urandom. (Я не можу коментувати, як вона порівнюється з точки зору криптографічно захищеного джерела випадковості, якщо це стосується вашого випадку використання).

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

Тепер дає:

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

назад до hexdumpвузького місця.

Оскільки у мене все ще є запасні процесори, я можу запустити 3 з них hexdumpпаралельно.

</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потрібне для оболонок, відмінних від zshзакритих команд 'stdin on / dev / null при запуску у фоновому режимі).

Зараз до 6,2 секунди, і мої процесори майже повністю використані.


3
Я просто видалив попередню відповідь і проголосував за цю. Я не отримав деяких вимог. Гарна відповідь btw.
Марсело

3
чому б не створити кілька цифр кожен пропуск? Навіть якщо ви читаєте в байтах за байтом, ви все одно можете створювати дві цифри кожен раз
phuclv

@ LưuVĩnhPhúc, я все perlодно видалив варіант, який був значно повільнішим. Я не можу отримати 2 цифри на байт за допомогою підходу tr | fold | paste.
Стефан Шазелас

Я afk, або я б спробував це самостійно, але ви можете спробувати перетворити 42 байти одночасно на 100-102 цифри, використовуючи bc(тоді вкажіть 0, 1 або 2 найбільш значущих цифр).
Ерік Тауерс

gitlab.com/ole.tange/tangetools/tree/master/rand генерує 1-4 Гб псевдовипадкових в секунду (якість AES).
Оле Танге

23

Якщо у вас є shufдоступ (останні GNU coreutils), ви можете зробити це:

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

На моєму комп'ютерному комп'ютері це зараз трохи повільніше, ніж відповідь Стефана, приблизно за 3: 4 фактором.


shufна моїй компанії PC не має -r, fmtНЕ -gнадто
phuclv

2
@ LưuVĩnhPhúc Yep - YMMV. Я виявив, що версії core-utils 8.25 мають ці, але 8.4 не мають. Яку версію ви використовуєте?
Цифрова травма

1
Я використовую coreutils 8.13
phuclv

@ StéphaneChazelas розумний paste/ printfхитрість - спасибі. Зараз ваша відповідь, мабуть, швидша.
Цифрова травма

17

Якщо вам не потрібна дуже якісна випадковість, і близький до рівномірного розподілу досить хороший, ви можете піти дуже швидко, особливо на сучасному процесорі з ефективними цілими векторами SIMD, як x86 з SSE2 або AVX2.

Це подібно до відповіді @ NominalAnimal, оскільки ми обидва мали однакову ідею, але вручну векторизувались на x86. (І з гіршими якісними випадковими числами, але все ще, мабуть, досить хорошими для багатьох випадків використання.) Це працює приблизно в 15 або 30 разів швидше, ніж код @ Nominal, при ~ 13 Гб / с ASCII на виході 2,5 ГГц Intel Haswell Процесор з AVX2. Це все ще менше, ніж теоретична максимальна пропускна здатність основної пам’яті (двоканальний DDR3-1600 - це близько 25,6 Гб / с), але я вчасно записував до / dev / null, тому насправді це просто перезапис буфера, який залишається гарячим у кеші. Skylake повинен виконувати цей самий код значно швидше, ніж Haswell (див. Нижню частину цієї відповіді).

Якщо припустити, що ви фактично є вузьким місцем на вході / виводу на диску або трубопроводі це десь, швидка реалізація означає, що ваш процесор навіть не повинен працювати на годиннику вище, ніж у режимі очікування. Він використовує набагато менше загальної енергії для отримання результату. (Термін служби акумулятора / тепло / глобальне потепління.)

Це настільки швидко, що ви, мабуть, не хочете записувати його на диск. Просто повторно генеруйте за потребою (з того ж насіння, якщо ви знову бажаєте однакових даних). Навіть якщо ви хочете подати його в багатопотоковий процес, який може використовувати всі процесори, запустивши це, щоб передати дані до нього, він залишить його гарячим в кеш-пам'яті L3 (і кеш-пам'ять L2 в ядрі, яка це написала), і використовувати це дуже небагато часу для процесора. (Але зауважте, що трубопровід додає багато накладних витрат, ніж написання /dev/null. На Skylake i7-6700k, переході до wc -cіншої програми, яка щойно читає + відкидає свій вхід, це приблизно в 8 разів повільніше, ніж запис/dev/null , і використовує лише 70% Процесор, але це ще 4,0 Гб / с на процесорі 3,9 ГГц.

Повторне генерування його швидше, ніж повторне зчитування, навіть із швидкого SSD, підключеного до PCIe, але IDK, якщо він більш енергоефективний (вектор-цілочисельний множник залишається досить зайнятим, і він, ймовірно, досить потужний, разом з іншими AVX2 256b векторних АЛУ). ОТОН, я не знаю, скільки часу для читання процесора з диска забирає щось, що максимізувало всі ядра, що обробляють цей вхід. Я здогадуюсь, що контекстний перемикач для повторного генерування в 128-кілограмових фрагментах може бути конкурентоспроможним із запуском файлової системи / коду кеш-сторінки та розподілом сторінок для читання даних з диска. Звичайно, якщо в кеш-пам’яті сторінок вже гаряче, це в основному memcpy. ОТО, ми вже пишемо про це так само швидко, як і memcpy! (яка повинна розділяти пропускну здатність основної пам'яті між читанням і записом). (Також зауважте, що запис на пам'ять, що "rep movsb(оптимізовано memcpy та memset у мікрокоді, що дозволяє уникнути RFO, оскільки впровадження його Енді Гліу в P6 (Pentium Pro) )).


Поки що це лише доказ концепції, а обробка нового рядка лише приблизно коректна. Це неправильно навколо кінців буфера потужності-2. Що більше часу на розробку. Я впевнений, що я міг би знайти більш ефективний спосіб вставити нові рядки, що також абсолютно правильно, з накладними витратами, принаймні настільки низькими, як це (порівняно з виведенням лише пробілів). Я думаю, це щось на зразок від 10 до 20%. Мене цікавить лише те, як швидко ми могли б зробити цей пробіг, а не мати насправді відшліфовану версію, тому я залишу цю частину як вправу для читача, з коментарями, що описують деякі ідеї.


На Haswell i5 з його максимальною турбіною 2,5 ГГц, з оперативною пам’яттю DDR3-1600MHz , приурочений до 100 Гбіт, але зменшився. (Призначено для cygwin64 на Win10 з gcc5.4 -O3 -march=native, пропущено, -funroll-loopsоскільки у мене було достатньо важкого часу, щоб отримати пристойні терміни запуску на цьому запозиченому ноутбуці. Потрібно тільки запустити Linux на USB).

запис у / dev / null, якщо не вказано інше.

  • Джеймс Холліс: (не перевірено)
  • Версія Fwrite номіналу: ~ 2.21с
  • це (SSE2): ~ 0,142 секунди ( несказаний час = реальний = 14,232s, користувач = 13,999s, sys = 0,187s).
  • це (AVX-128): ~ 0,140с
  • це (AVX2): ~ 0,073s (без масштабу : real = 0m7,291s, користувач = 0m7,125s, sys = 0m0,155s).
  • це (AVX2) цигвін-трубопровід wc -c, з розміром буфера 128 Кб: 0,32 с процесором на 2,38 ГГц (макс. двоядерний турбо). (Нерозрахунковий час: реальний = 32.466s користувач = 11.468s sys = 41.092s, включаючи і це, і те wc). Лише половину даних було фактично скопійовано, оскільки моя дурна програма передбачає, що запис виконує повний буфер, хоча це не так, а cygwin write () складає лише 64 к за один дзвінок у трубу.

Так що з SSE2 це приблизно в 15 разів швидше, ніж скалярний код @Nominal Animal. З AVX2 це приблизно в 30 разів швидше. Я не пробував версію коду Nominal, яка просто використовується write()замість fwrite(), але, імовірно, для великих буферів stdio здебільшого не виходить із шляху. Якщо це копіювання даних, це призведе до значного уповільнення.


Час для отримання 1 Гб даних на Core2Duo E6600 (Merom 2,4 ГГц, 32кіБ приватний L1, 4MiB спільний кеш L2), DDR2-533 МГц у 64-бітному Linux 4.2 (Ubuntu 15.10). Незважаючи на те, що розмір буфера розміром 128 Кб для write (), не дослідив цей вимір.

запис у / dev / null, якщо не вказано інше.

  • (SSE2) це з обробкою нового рядка та 4 векторами цифр від кожного вектора випадкових байтів: 0,183s (приурочено до 100GiB за 18,3s, але аналогічні результати для запуску 1GiB). 1,85 інструкцій за цикл.
  • (SSE2) це, посилаючись на wc -c: 0,593s (без масштабу : real = 59,266s user = 20,148s sys = 1m6,548s, включаючи час процесора wc). Така ж кількість системних викликів write (), як і у cygwin, але насправді переносить усі дані, оскільки Linux обробляє всі 128k запису () в трубу.
  • NominalAnimal в fwrite()версії (gcc5.2 -O3 -march=native), що запускаються з ./decdig 100 $((1024*1024*1024/200)) > /dev/null: 3.19s +/- 0,1%, з 1,40 інструкції за один цикл. -funroll-петлі зробили, можливо, невелику різницю. clang-3.8 -O3 -march=native: 3,42s +/- 0,1%
  • Nominal- fwriteтрубопроводів для wc -c: реальний = 3.980s користувач = 3.176s SYS = 2.080s
  • clang++-3.8 -O3 -march=nativeПовна версія Джеймса Холліса ( ): 22,885s +/- 0,07%, з 0,84 інструкціями за цикл. (г ++ 5,2 було трохи повільніше: 22,98 с). Запис лише одного рядка за один раз, ймовірно, значно зашкодить.
  • Стефан Шазелас tr < /dev/urandom | ...: реальний = 41.430s користувач = 26.832s sys = 40.120s. trбільшу частину часу отримував все ядро ​​центрального процесора, витрачаючи майже весь свій час на драйвер ядра, генеруючи випадкові байти та копіюючи їх у трубу. Інший сердечник на цій двоядерній машині пройшов решту конвеєра.
  • time LC_ALL=C head -c512M </dev/urandom >/dev/null: тобто просто читати стільки випадкових випадків без трубопроводів: real = 35.018s user = 0.036s sys = 34.940s.
  • Програма Perl Lưu Vĩnh Phúc (perl v5.20.2 від 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. Ще дуже повільно.

  • (SSE2) це без обробки нового рядка і 3 або 4 вектори цифр від кожного вектора випадкових байтів (майже точно однакова швидкість: dig3 = v%10крок приблизно беззбитковості на цьому HW): 0,166s (1,82 інструкції за цикл) . Це в основному нижня межа для того, що ми можемо наблизитись до ідеально ефективної обробки нового рядка.

  • (SSE2) Стара версія цього без будь - якої обробки нового рядка, але тільки отримувати одну цифри за uint16_t елемента з допомогою v%10, 0,222 секунд +/- 0,4%, 2,12 інструкцій за такт. (Скомпільовано з gcc5.2,. Розмотування -march=native -O3 -funroll-loopsциклів відбувається, щоб допомогти цьому коду на цьому обладнання. Не використовуйте його наосліп, особливо для великих програм).
  • (SSE2) Стара версія цього, запис у файл (на RAID10f2 з 3 швидких магнітних жорстких дисків, не дуже оптимізованих для запису): ~ 4 секунди. Можна піти швидше, налаштувавши параметри буфера вводу-виводу ядра, щоб дозволити набагато більше брудних даних перед блоками write (). "Системний" час все ще ~ 1,0 секунди, набагато більший, ніж "користувальницький" час. У цій старій системі з повільною оперативною пам’яттю DDR2-533 оператору потрібно ~ 4 рази більше, щоб ядро ​​запомняло дані в кеш сторінки і запускало функції XFS, ніж це робиться для мого циклу, щоб тримати перезапис його на місці в буфер, який залишається гарячим в кеш.

Як це робиться

Швидкий PRNG очевидно важливий. xorshift128 + може бути векторизований, тому у вас є два або чотири 64-бітні генератори паралельно, в елементах SIMD-вектора. Кожен крок виробляє повний вектор випадкових байтів. ( Тут реалізована 256b реалізація AVX2 із вбудованими технологіями Intel ). Я обрав це за вибором Номінального числа xorshift *, тому що 64-бітове множення векторних цілих чисел можливе лише в SSE2 / AVX2 із застосуванням розширеної точності .


Враховуючи вектор випадкових байтів, ми можемо порубати кожен 16-бітний елемент на кілька десяткових цифр. Ми виробляємо декілька векторів 16-бітних елементів, кожен з яких є одним ASCII цифрою + пробілом ASCII . Ми зберігаємо це безпосередньо у вихідному буфері.

Моя оригінальна версія щойно використовується x / 6554для отримання однієї випадкової цифри від кожного елемента uint16_t вектора. Це завжди між 0 і 9 включно. Це упереджено 9, бо (2^16 -1 ) / 6554це лише 9,99923. (6554 = ceil ((2 ^ 16-1) / 10), що забезпечує коефіцієнт завжди <10)

x/6554можна обчислити з одним множенням на "магічну" константу ( зворотна фіксована точка ) і правильний зсув результату з високою половиною. Це найкращий випадок ділення на постійну; деякі підрозділи проводять більше операцій, а підписаний відділ вимагає додаткової роботи. x % 10має подібний ухил і не настільки дешевий для обчислення. (вихідний сигнал ASC еквівалентний x - 10*(x/10), тобто додаткове множення і віднімання вгорі ділення за допомогою модульної мультиплікативної зворотної.) Також найнижчий біт xorshift128 + не настільки високої якості , тому краще ділити для отримання ентропії від високих бітів ( для якості, а також швидкості), ніж модуль, щоб взяти ентропію з низьких біт.

Однак ми можемо використовувати більше ентропії в кожній uint16_t, переглядаючи низькі десяткові цифри, як-от digit()функція @ Nominal . Для досягнення максимальної продуктивності я вирішив взяти низькі 3 десяткових цифри і x/6554, щоб зберегти одну PMULLW та PSUBW (і, мабуть, деяку MOVDQA) проти вищої якості, щоб взяти 4 низьких десяткових цифри. x / 6554 незначно позначається на 3-х десяткових цифрах, тому існує деяка кореляція між цифрами від одного і того ж елемента (8 або 16 розрядів на виході ASCII, залежно від ширини вектора).

Я думаю, що gcc ділиться на 100 та 1000, а не довший ланцюг, який послідовно ділиться на 10, тому, ймовірно, не суттєво скорочується довжина ланцюга залежностей, що не переносяться циклами, що дає 4 результати з кожного виходу PRNG. port0 (вектор множення та зміщення) - це вузьке місце через модульні мультиплікативні обертання та зрушення в xorshift +, тому, безумовно, корисно зберегти вектор-множення.

xorshift + настільки швидкий, що навіть використання лише ~ 3,3 біт випадковості від кожні 16 (тобто ефективність 20%) не набагато повільніше, ніж їх розбивання на кілька десяткових цифр. Ми лише наближаємо рівномірний розподіл, оскільки ця відповідь орієнтована на швидкість, доки якість не надто погана.

Будь-яка умовна поведінка, яка зберігає змінну кількість елементів, зайняла б набагато більше роботи. (Але, можливо, це все-таки можна зробити дещо ефективніше, використовуючи методи лівого пакування SIMD . Однак, це стає менш ефективним для невеликих розмірів елементів; таблиці пошуку гігантських перетасовок-масок не є життєздатними, і немає перемикання смуг AVX2 з розміром менше 32- бітові елементи. Версія PSHUFB 128b, можливо, все ще зможе генерувати маску під час руху з BMI2 PEXT / PDEP, як і для AVX2 з більшими елементами , але це складно, оскільки 64-бітове ціле число вміщує лише 8 байт. Посилання Godbolt у цьому відповіді є якийсь код, який може працювати для більшої кількості елементів.)


Якщо затримка RNG - це вузьке місце, ми можемо піти ще швидше, запустивши паралельно два вектори генераторів, чергуючи той, який ми використовуємо. Компілятор все ще може легко зберігати все в регістрах у розкрученому циклі, і це дозволяє обидва ланцюги залежності працювати паралельно.

У поточній версії, підбиваючи вихід PRNG, ми фактично вузьке місце на пропускній спроможності порту 0, а не затримку PRNG, тому в цьому немає необхідності.


Код: версія AVX2

Повна версія з додатковими коментарями щодо провідника компілятора Godbolt .

Не дуже охайно, вибачте, що я мушу спати і хочу опублікувати це.

Щоб отримати версію SSE2, s/_mm256/_mm, s/256/128/, s/v16u/v8u/, і змінити vector_size(32)до 16. Крім того, змінити приріст нового рядка з 4 * 16 на 4 * 8. (Як я вже говорив, код безладний і не надто налаштований для компіляції двох версій. Спочатку не планував робити версію AVX2, але тоді я дуже хотів протестувати на процесорі Haswell, до якого я мав доступ.)

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

}

Компілюйте з gcc, clang або ICC (або, сподіваємось, будь-яким іншим компілятором, який розуміє діалект CN GNU C99 та інтеграли Intel). Векторні розширення GNU C дуже зручні для отримання компілятора для генерації магічних чисел для поділу / модуля за допомогою модульних мультиплікативних обертів, а випадкові __attribute__s корисні.

Це можна записати портативно, але знадобиться більше коду.


Примітки щодо виконання:

Перекриття магазину для вставки нових рядків має значні накладні витрати, щоб вирішити, де його розмістити (непередбачувані гілки та вузькі місця в Core2), але сам магазин не впливає на продуктивність. Коментуючи саме цю інструкцію щодо зберігання в ASM компілятора (залишаючи всі розгалуження однаковими), залишилася продуктивність на Core2 повністю незмінною, при цьому повторні запуски давали в той же час +/- менше 1%. Тож я роблю висновок, що буфер магазину / кеш-пам'яті справляється з ним просто чудово.

Однак використання якогось обертового вікна ascii_digitspaceз одним елементом, що має новий рядок, може бути ще швидшим, якщо ми розкрутимо достатньо, щоб будь-які лічильники / розгалуження відійшли.


Запис у / dev / null в основному не працює, тому буфер, ймовірно, залишається гарячим у кеш-пам'яті L2 (256кіБ на ядро ​​Haswell). Очікується ідеальна швидкість руху від 128b до 256b векторів: додаткових інструкцій немає, і все (включаючи магазини) відбувається з подвійною шириною. Однак гілка введення нового рядка береться вдвічі частіше, хоча. Я, на жаль, не встиг про встановити Хасвелл Cygwin з цією частиною #ifdef.

2,5 ГГц * 32В / 13,7 Гб / с = 5,84 циклів в магазині AVX2 на Haswell. Це досить добре, але може бути швидше. Можливо, в системі цигвін є якісь накладні витрати, ніж я думав. Я не намагався коментувати їх у виході ASM компілятора (що б гарантувало, що нічого не оптимізоване.)

Кеш-пам'ять L1 може підтримувати один запам'ятовуючий пристрій 32В на добу, а L2 - не набагато менша пропускна здатність (хоча більша затримка).

Коли я переглянув IACA кілька версій тому (без розгалуження для нових рядків, але отримуючи лише один вектор ASCII на RNG-вектор), він передбачив щось подібне до одного магазину векторів 32В на 4 або 5 годин.

Я сподівався отримати більшу швидкість, ніж витягнути більше даних з кожного результату RNG, базуючись на перегляді asm, враховуючи посібники Agner Fog та інші ресурси оптимізації, на які я додав посилання у вікі тегів SO x86 .)

Ймовірно, це було б значно швидше на Skylake , де векторне ціле число множення та зсув може працювати на два рази більше портів (p0 / p1) порівняно з Haswell (лише p0). xorshift і видобуток цифр використовують багато змін і множень. ( Оновлення: Skylake запускає його на 3,02 IPC, даючи нам 3,77 циклів на 32-байтовому сховищі AVX2 , приуроченому до 0,030 секунди за 1 Гб ітерації, записуючи /dev/nullна Linux 4.15 на i7-6700k на частоті 3,9 ГГц.


Для його роботи не потрібен 64-бітний режим . Версія SSE2 настільки ж швидка при компіляції -m32, оскільки їй не потрібно дуже багато векторних регістрів, і вся 64-бітова математика проводиться у векторах, а не в регістрах загального призначення.

Насправді це трохи швидше в 32-розрядному режимі на Core2, тому що порівняння / гілка макро-синтезу працює лише в 32-бітному режимі, тому менше ядра для ядра поза замовленням (18,3 секунди (1,85 інструкції за такт) vs 16,9s (2,0 IPC)). Менший розмір коду відсутності префіксів REX також допомагає декодерам Core2.

Крім того, деякі переміщення вектора reg-reg замінюються навантаженнями, оскільки не всі константи фіксуються у векторних рег. Оскільки пропускна здатність навантаження з кешу L1 не є вузьким місцем, це фактично допомагає. (наприклад, множення на постійний вектор set1(10): movdqa xmm0, xmm10/ pmullw xmm0, xmm1перетворюється на movdqa xmm0, [constant]/ pmullw xmm0, xmm1.) Оскільки для reg-reg MOVDQA потрібен порт ALU, він конкурує з реальною роботою, яка виконується, але навантаження MOVDQA конкурує лише за пропускну здатність декодування переднього кінця. (Наявність 4-байтної адреси всередині багатьох інструкцій скасовує велику вигоду від збереження префіксів REX.

Я не був би здивований, якщо врятувати ALU MOVDQA Uops - це те, звідки беруться справжні вигоди, оскільки фронтенд повинен бути в ногу з середнім рівнем 2.0 IPC.

Всі ці відмінності зникають на Haswell, де вся справа повинна запускатися з кешованого декоду взагалі кеша, якщо не буфера зворотного зв'язку. ALU + макро-синтез гілки працює в обох режимах з часів Негалема.


6
Мені просто подобається, як ти перейшов "режим звіра" в тему! :) Що ще важливіше, це прекрасний приклад того, які вигоди можна отримати, якщо вам справді потрібно чи хочете вичавити максимальну продуктивність, використовуючи дуже низькі знання обладнання, що є у вас. Плюс, ми тут використовуємо лише одну нитку; Більшість поточних процесорів Intel / AMD для настільних та серверів (і навіть ARM у легких планшетах та SBC) мають декілька ядер, тому є ще доступні більше прискорення в реальному часі. І нарешті, наскільки непрактичним є "найшвидший спосіб" запитувань, зумовлений чистими зусиллями.
Номінальна тварина

1
@NominalAnimal: Так, навіть повільний чотиримісний або основний ядро ​​ARM може легко наситити пропускну здатність основної пам’яті, роблячи те саме, що й NEON (навіть якщо вони були підключені до швидкого двоканального DDR3), якщо він має 64-бітні цілочисельні SIMD додавання та зсуви . Я припускаю, що NEON має 16-розрядний множник розміру елементів для роботи з аудіо. Планування інструкцій було б набагато більшою роботою для ARM на замовлення, оскільки кожна ітерація ланцюга залежностей, що переносяться циклом (xorshift128 +), подає кілька незалежних ланцюгів залежностей від подрібнення та зберігання в пам'яті ...
Петро Корди

... Виконання поза замовленням їсть це на сніданок, тому що вся справа досить коротка, щоб декілька ітерацій помістилися в ROB (192 уп на HSW IIRC). (тобто "вікно" інструкцій, яке бачить виконання поза замовлення, включає кілька ітерацій). Таким чином, процесор може закінчувати остаточне сховище за 2 або 3 ітерації тому, а також починати з початку поточної ітерації. Це приховує затримку незалежних ланцюгів, тому має значення лише пропускна здатність. Для ядра порядку, це потребує конвеєрного програмного забезпечення ...
Пітер Кордес

... Хороший компілятор ARM повинен зробити щось із цього для вас, якщо ви пишете це з внутрішніх текстів (або GNU C натиснений синтаксис векторів для всієї справи, як я мав би зробити це в першу чергу). Я не маю досвіду робити це реально, тому вам може знадобитися помасажувати цикл і, можливо, зробити трохи ручного розкручування / конвеєрного забезпечення програмного забезпечення в джерелі, щоб отримати гарну тривогу. (Є декілька ядер ARM, що не входять в порядок, знайдені в телефонах вищого класу, але вони не мають такого великого вікна поза замовлення, як Haswell. У OTOH вони також мають меншу пропускну здатність, тому менше виграти від пошуку більшої кількості ІЛП).
Пітер Кордес

1
@NominalAnimal: також погодився з глузливістю питання. "Найшвидший" без обмежень щодо якості випадковості - нерозумно ... З BTRFS одні й ті ж дані на диску можуть бути частиною файлу кілька разів (див. EXTENT_SAME в 4.2 ). Таким чином, ви можете генерувати випадкові 4кіБ або 1МБ і повторити це. Це випадковість з коротким періодом, але вона все ще випадкова і коштує лише метаданих вводу / виводу. (Власне, вам потрібно, щоб блок закінчувався новим рядком. Lcm (4096, 4096 * 200) = 4096 * 200 = 819200 = 800кіБ, тому ваш повторюваний блок є будь-яким кратним цього.)
Пітер Кордес

14

Ось рішення, яке я сподіваюся, зрозуміти просто:

od -An -x /dev/urandom | tr -dc 0-9 | fold -w100 | awk NF=NF FS= | head -c1G
  • odстворює рівномірний потік шістнадцяткових цифр від /dev/random.
  • trпозбавляється від букв, зберігаючи лише 0-9цифри
  • fold гарантує наявність 100 цифр на рядок
  • awk вставляє пробіли всередині ліній
  • head обрізає вхід на 1 гігабайт

2
Це хороший альтернативний спосіб отримання більш ніж однієї цифри в байтах / dev / random, при цьому все ще має рівномірний розподіл, який виробляє 320 цифр на кожні 256 байт / dev / urandom в середньому (менше, ніж при перетворенні байтів <200 модуля 100 до десяткової, що дає 400 цифр на кожні 256 байт).
Стефан Шазелас

6

Ви можете використовувати jotкоманду для цього:

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

1
@DigitalTrauma У моїй версії fmtнемає опції ширини цілі. У будь-якому випадку, це буде точно, тому що всі цифри займають рівно один стовпець!
садок

Для запису моя fmtверсія fmt (GNU coreutils) 8.25(Ubuntu 16.04)
Digital Trauma

2
правильне число на пів gb: 1024 * 1024 * 1024/2 =536870912
Олів'є Дулак

1
@OlivierDulac Залежить, про який "гігабайт" йдеться. Деякі люди використовують 1 Gb, щоб означати 10 ^ 9 замість 2 ^ 30, навіть якщо це технічно неправильно. Плюс мені подобаються хороші круглі номери :)
садок

6
@gardenhead, все більше людей зараз схильні переходити на Гігабайт == 1e9 та Gibibyte == 2 ^ 30, оскільки це стандартне визначення IEC. Дивіться Вікіпедію . Зверніть увагу , що саме по собі Gb волів би бути гіга трохи .
Стефан Шазелас

6

Це схоже на метод Стефана Шазеласа, проте я читав 64 біти одразу для підвищення продуктивності. Розподіл все ще є рівномірним, але тепер ви отримуєте 19 цифр на кожні 8 байт замість лише 8 у кращому випадку, як раніше

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

На 32-розрядної платформі замість 19 буде читатися 9 цифр кожного разу.


Це може призвести до винятку, якщо ваша система не підтримує 64-розрядне ціле число або perlне компілюється з підтримкою quad.
cuonglm

@cuonglm так, як я вже сказав, якщо Perl не має 64 біт у цій системі, тоді програму потрібно змінити, next if $n >= 1000000000; $s = sprintf("%09u", $n);щоб отримати лише 9 цифр
phuclv

Ви не можете, програма вийде з ладу, $n = unpack("Q")якщо Quad не підтримується.
cuonglm

1
@cuonglm змінити BEGIN{$/=\4; $,=" "} $n = unpack("L");також
phuclv

1
Вибачте, але це 19 цифр із 8-байтного введення лише приблизно 54,2% часу, і нічого іншого, в середньому 1,29 цифри на вхідний байт. Якщо ви більше використовуєте Stephane <16e18і ділите на 16, ви отримаєте 18 цифр 86,7% за 1,95 dpB. Маючи 32- <4e9 /4бітовий , отримує 9 цифр 93,1% за 2,10 дпБ. Але 5 байт (як шістнадцятковий (H10)) <1e12дає 12 розрядів 90,9% за 2,18 dpB, або розділивши шістнадцять навпіл і виконавши кожну половину, <1e6 дає 6 цифр 95,4% за 2,29 dpB; це наближається до межі log_10 (256) = 2,41.
dave_thompson_085

3

Я якось погоджуюся з Nominal Animal у використанні складеної мови програмування, якщо вам потрібна швидкість. Однак вам не потрібно писати власний код RNG в C. C ++ 11 пропонує чудовий Mersenne Twister як частину його стандартної бібліотеки.

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

Вищевказаний код досить простий і займає близько хвилини, коли я передаю висновок у файл. Ми можемо пройти набагато швидше, створивши рядок, достатньо великий для 100 цифр, і зламати цифри в нього. Це дозволяє нам викликати cout кожен рядок, а не кожну цифру.

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

Цей код займає мою машину близько шести секунд. Пам'ятайте, що це стандартний вихід, тому передайте його у файл.

У мене є пара відмови. По-перше, я пишу це на ПК з Windows. Я думаю, що всі бібліотеки присутні в Linux, але якщо я помиляюсь, обов'язково зазначте це.

Крім того, він фактично видає рівно півмільярда цифр, розділених простором, що технічно є гігабайт, але, можливо, не зовсім те, що ви хотіли. Він виводить 5 мільйонів рядків, 100 цифр на рядок. Якщо різниця важлива, можна збільшити кількість рядків. У моєму вікні Windows файл здається трохи більшим, ніж 10 ^ 9 байт, що, на мою думку, пов'язане з додатковими символами нового рядка.


2
Гей, критика насправді не справедлива! :) Більшість моєї програми - це аналіз параметрів командного рядка. Якщо я також опущу коментарі, перевірку помилок і жорсткий код кількості виведених стовпців і рядків, я можу зробити його менше ніж удвічі більшим за розмір вашого коду - навряд чи монстро . :) Заперечуємо: так, бібліотеки доступні у більшості дистрибутивів Linux. На моєму ноутбуці у вас час від часу триває близько 14 секунд, тоді як у моєї версії за часом працює всього 1,3 секунди. Різниця лише завдяки PRNG: Mersenne Twister є набагато повільніше, ніж Xorshift64 *.
Номінальна тварина

1
Я хотів би зазначити одну практичну річ, яку ви пропустили, але, я сподіваюся, ви не сприймаєте це як негатив, просто щось над цим подумати: Як я вже згадував у своїй відповіді, програми з одноматеріалом рідко варті того час, який вони потребували для написання. Ось чому додавати синтаксичний розбір командного рядка та текст використання довідки майже завжди варто. У мене є великий набір таких корисних програм, і замість того, щоб полювати на їх джерела, щоб дізнатися, що робить кожен з них, я просто запускаю їх, тому вони мені скажуть; і я можу змінити їх поведінку достатньо, щоб задовольнити більш ніж одну потребу. Амортизуючі витрати на розробку.
Номінальна тварина

@NominalAnimal Ще одна важлива річ - це те, що ви переклали вихід, до /dev/nullякого було б набагато швидше, ніж записувати в реальний файл
phuclv

@ LưuVĩnhPhúc: Ну не дуже. У цього ноутбука встановлений SSD на 128 Гб, з ~ 500 Мб / с послідовно читає і записує. Поставте чотири в конфігурацію програмного забезпечення Linux-RAID0, і ви отримаєте набагато більше, ніж гігабайт секунди читає і записує при генерації таких великих наборів даних (я оцінюю ~ 1,75 ТБ / с). 1 Гб / с досягнуто років тому за допомогою 12 накопичувачів SATA (спінінг-плати, навіть не SSD) з Linux sw-RAID0. (Примітка: байт / с, а не біт / с.) Звичайно, для "нормальної" машини це звучить нерозумно, але ті, хто грає з великими наборами даних, вважають, що це заслуговує на себе - ви голієте час, що робите (з великими наборами даних) цей шлях.
Номінальна тварина

1
@NominalAnimal і Lu'u: Що ще важливіше, якщо у вас достатньо оперативної пам’яті, програма може вийти задовго до того, як всі дані будуть на диску. Більша частина роботи у великому write()системному виклику - це memcpy у кеш сторінок, який блокується лише у тому випадку, якщо ядро ​​вирішить зробити це замість виділення більше буферного простору. Ця програма повинна бути вузьким місцем на вході / виводу диска лише тоді, коли пам'ять є обмеженою або якщо ви використовували O_DIRECT для обходу кеш-сторінки. Якщо ви розміщені write()в шматках, менших за розмір кешу, сподіваємось, ваші дані лише один раз перейдуть у основну пам'ять, а буфер, який переписаний на місці, залишається гарячим у кеш-пам'яті L2 або L3.
Пітер Кордес

1

Це залежить від вашого визначення поняття "випадковий". Якщо ви маєте на увазі криптографічно випадкові випадки, вам просто потрібно отримати хорошу бібліотеку і кусати кулю, дочекатися її запуску.

Якщо вам просто потрібно щось, що виглядає досить випадково, ось простий спосіб:

  1. Отримайте файл довжиною кілька Gb. Ваш улюблений фільм буде хорошим.
  2. Gzip це, простий спосіб видавити повторні візерунки
  3. Пройдіть по файлу одночасно nybble (півбайта). Кожне значення буде від 0 до 15. Викиньте менше 1 або більше 10. Відніміть 1 з кожного з перших мільярдів уцілілих і запишіть його як цифру.

Біг на повільній машині може зайняти годину; досить швидко і досить випадково для більшості цілей.


9
/dev/urandomшвидше за все gzip, як у швидкості, так і у випадковості.
Stig Hemmer

Get a file that is several Gb longвам знадобиться файл ** принаймні 8Gb`, щоб отримати файл 1 Гб
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
Ласкаво просимо на сайт! Дивіться посилання на моїй сторінці профілю. Тут є дуже багато питань, які я бачу майже універсально у скриптах оболонок, але це не робить їх правильними.
Wildcard

2
@Wildcard: ніколи, cat file | trколи можна просто tr <file. IIRC, можна навіть <file tr. Я думав, що ви просто говорили про цей скрипт оболонки, виглядаючи незграбно і повільно, як du | awkпісля кожного рядка, щоб перевірити розмір, і повторно відкрити файл для додавання кожного рядка, а не перенаправляти за межі циклу.
Пітер Кордес

2
@PeterCordes, так. Чому використання циклу оболонки для обробки тексту вважається поганою практикою? є особливо актуальним - цей сценарій заснований на ідеї, що Bash - мова програмування, така як C, що це не так. Але, \ @NamNT, я сподіваюся, що ви затримаєтесь на цьому веб-сайті, оскільки зрозуміло, що у вас дуже логічний розум. :)
Wildcard

4
@PeterCordes cat /dev/urandom | busy-cmd- один з тих рідкісних випадків, коли він може мати сенс, оскільки він може розділити випадкові генерації та зайняті cmd між процесорами. Не стільки для tr, скільки, наприклад, має значення для Sam od.
Стефан Шазелас

1
@ StéphaneChazelas: о так! Так, системний виклик read () - це місце, де витрачається час процесора RNG.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.