Какой самый быстрый способ создания текстового файла размером 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
Вы, вероятно, должны сказать, что вы подразумеваете под «случайным» - криптографическая сила случайная или адекватна ли псевдослучайная последовательность?
Тоби Спейт

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 * и метод исключения, чтобы избежать смещения цифр.)

десятичной digits.c:

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

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

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

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

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

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

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

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

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

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

    return result - UINT64_C(1);
}

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

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

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

    return retval;
}

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

    if (!src)
        return errno = EINVAL;

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

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

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

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

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

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

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

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

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

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

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

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

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

    return EXIT_SUCCESS;
}

Мы можем сделать это намного быстрее, если переключимся на строковый буфер и 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как того требует OP.

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

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

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

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


Да, возможно, mmap()это самый простой путь к лучшей скорости ввода-вывода - но это эталонный тест, прежде чем делать какие-либо заявления!
Тоби Спейт

@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) в «x», который мы затем отбросить (с 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 байт / dev / urandom со скоростью 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

Больше процессорного времени в целом, но лучше распределяется между моими четырьмя процессорами, так что в итоге требуется меньше настенного времени. Узкое место сейчас 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необходим для оболочек, отличных от zshstdin команд закрытия, которые находятся в / dev / null при запуске в фоновом режиме).

Теперь до 6,2 секунды, и мои процессоры почти полностью использованы.


3
Я просто удалил свой предыдущий ответ и проголосовал за него. Я не получил некоторые требования. Хороший ответ, кстати.
Марсело

3
почему вы не генерируете несколько цифр каждый проход? Даже если вы читаете побайтово, вы все равно можете выводить по 2 цифры каждый раз
phuclv

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

Я афк или я бы попробовал это сам, но вы можете попробовать преобразовать 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 Да - YMMV. Я обнаружил, что в версии 8.25 core-utils они есть, а в 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 векторных ALU). OTOH, я не знаю, сколько процессорного времени чтения с диска отнимает у чего-то, что максимизирует все ядра, обрабатывающие этот ввод. Я полагаю, что переключение контекста для повторной генерации в кусках по 128 КБ может быть конкурентоспособным с запуском кода файловой системы / кэша страниц и выделением страниц для чтения данных с диска. Конечно, если в страничном кэше уже жарко, то это просто memcpy. ОТО, мы уже пишем о так быстро, как memcpy! (который должен разделять пропускную способность основной памяти между чтением и записью). (Также обратите внимание, что запись в память, чтоrep movsb(оптимизированы memcpy и memset в микрокоде, что позволяет избежать RFO, поскольку Энди Глью реализовал его в P6 (Pentium Pro) )).


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


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

запись в / dev / null, если не указано иное.

  • Джеймс Холлис: (не проверено)
  • Номинальная версия fwrite: ~ 2.21 с
  • это (SSE2): ~ 0,142 с ( немасштабированное время = реальное = 14,232 с, пользователь = 13,999 с, sys = 0,187 с).
  • это (AVX-128): ~ 0,140 с
  • это (AVX2): ~ 0,073 с ( немасштабированное : действительное = 0m7,291 с, пользовательское = 0m7,125 с, sys = 0m0,155 с).
  • это (AVX2) трубопровод wc -cCygwin с размером буфера 128 кБ: 0,32 с процессором на частоте 2,38 ГГц (макс. двухъядерный Turbo). (немасштабированное время: реальное = 32,466 с, пользователь = 11,468 с, с = 41,092 с, включая это и wc). Правда, только половина данных была скопирована, потому что моя глупая программа предполагает, что запись выполняет полный буфер, хотя это не так, и cygwin write () делает только 64 КБ на вызов в канал.

Так что с SSE2 это примерно в 15 раз быстрее, чем скалярный код @Nominal Animal. С AVX2 это примерно в 30 раз быстрее. Я не пробовал версию кода Nominal, которая просто использует write()вместо fwrite(), но, по-видимому, для больших буферов stdio в основном остаётся в стороне. Если это копирование данных, это будет причиной большого замедления.


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

запись в / dev / null, если не указано иное.

  • (SSE2) это с обработкой новой строки и 4 векторами цифр из каждого вектора случайных байтов: 0,183 с (при времени 100 ГБ за 18,3 с, но аналогичные результаты для прогонов 1 ГБ). 1,85 инструкции за цикл.
  • (SSE2) это, трубопровод wc -c: 0,593 с ( немасштабированный : реальный = 59,266 с, пользователь = 20,148 с, sys = 1m6,548 с, включая время процессора в wc). Такое же количество системных вызовов write (), как и в cygwin, но на самом деле передает все данные, потому что Linux обрабатывает все 128k write () в канал.
  • 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.42 с +/- 0,1%
  • Номинальный fwriteтрубопровод wc -c: реальный = 3,980 с, пользователь = 3,176 с, sys = 2,080 с.
  • clang++-3.8 -O3 -march=nativeЛинейная версия Джеймса Холлиса ( ): 22,885 с +/- 0,07%, с 0,84 инструкциями за цикл. (g ++ 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,166 с (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 * в Nominal, потому что 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имеет аналогичное смещение и не так дешево вычислять. (Вывод asm для gcc эквивалентен 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- битовые элементы. 128-битная версия PSHUFB все еще может генерировать маску на лету с BMI2 PEXT / PDEP, как вы можете для AVX2 с более крупными элементами , но это сложно, потому что 64-битное целое число содержит только 8 байтов. в этом ответе есть код, который может работать для большего количества элементов.)


Если задержка ГСЧ является узким местом, мы могли бы пойти еще быстрее, запустив два вектора генераторов параллельно, чередуя один из которых мы используем. Компилятор все еще может легко хранить все в регистрах в развернутом цикле, и это позволяет двум цепочкам зависимостей работать параллельно.

В текущей версии, сокращая выход 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 (или, надеюсь, любого другого компилятора, который понимает диалект C GNU C от C99 и присущи Intel). Векторные расширения GNU C очень удобны для того, чтобы компилятор генерировал магические числа для деления / по модулю, используя модульные мультипликативные инверсии, и случайные __attribute__s полезны.

Это может быть написано переносимо, но это займет больше кода.


Примечания по производительности:

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

Тем не менее, использование некоторого вида вращающегося окна ascii_digitspaceс одним элементом, имеющим символ новой строки, может быть даже быстрее, если мы развернем достаточно, чтобы исчезли какие-либо счетчики / ответвления.


Запись в / dev / null в основном не работает, поэтому, вероятно, буфер остается горячим в кеше L2 (256 кБ на ядро ​​в Haswell). Ожидается идеальное ускорение от 128b векторов до 256b векторов: никаких дополнительных инструкций нет, и все (включая магазины) происходит с удвоенной шириной. Тем не менее, ветвь вставки новой строки используется в два раза чаще. К сожалению, у меня не было времени на настройку Haswell cygwin с этой частью #ifdef.

2,5 ГГц * 32B / 13,7 ГБ / с = 5,84 цикла на магазин AVX2 в Haswell. Это довольно хорошо, но может быть быстрее. Возможно, в системных вызовах cygwin есть некоторые издержки, чем я думал. Я не пытался комментировать их в выводе asm компилятора (что гарантировало бы, что ничего не оптимизировано).

Кэш-память L1 может поддерживать одно хранилище 32B за такт, а уровень L2 не намного ниже пропускной способности (хотя и более высокая задержка).

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

Я надеялся получить больше ускорения от извлечения большего количества данных из каждого результата ГСЧ, основываясь на том, как я сам смотрю на ассемблер, учитывая руководства Агнера Фога и другие ресурсы по оптимизации, ссылки на которые я добавил в вики-тэге SO x86 .)

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


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

На самом деле это немного быстрее в 32-битном режиме на Core2, потому что слияние / ветвление макросов работает только в 32-битном режиме, поэтому меньше ошибок в ядре из строя (18,3 с (1,85 инструкций за такт) против 16,9 с (2,0 МПК)). Меньший размер кода из-за отсутствия префиксов 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 в мопах, поскольку внешний интерфейс должен очень хорошо отставать от среднего значения 2,0 IPC.

Все эти различия исчезают в Haswell, где все это должно запускаться из кэша decoded-uop, если не из буфера обратной петли. ALU + ветвление макро-синтеза работает в обоих режимах начиная с Nehalem.


6
Я просто люблю, как вы пошли "режим зверя" в тему! :) Что еще более важно, это отличный пример того, какие выгоды доступны, если вам действительно нужно или вы хотите выжать максимальную производительность, используя очень низкое знание оборудования под рукой. Плюс, мы используем только одну нить здесь; большинство современных процессоров Intel / AMD для настольных ПК и серверов (и даже процессоры ARM в облегченных планшетах и ​​SBC) имеют несколько ядер, поэтому в настоящее время доступно еще больше ускорений в реальном времени. И, наконец, насколько непрактичным является «самый быстрый способ» вопросов из-за приложенных усилий.
Номинальное животное

1
@NominalAnimal: Да, даже медленный четырехъядерный или окто-ядерный ARM может легко насытить пропускную способность основной памяти, делая то же самое с NEON (даже если они подключены к быстрому двухканальному DDR3), если он имеет 64-битное целочисленное добавление и сдвиг SIMD , Я предполагаю, что NEON имеет 16-битное умножение на размер элемента для работы с аудио. Планирование инструкций будет гораздо больше работы для ARM в порядке, потому что каждая итерация цепочки зависимостей, переносимых циклами (xorshift128 +), подает несколько независимых цепочек зависимостей, которые разбираются и передаются в память ...
Питер Cordes

... Внеочередное выполнение съедает это на завтрак, потому что все достаточно коротко, чтобы в ROB поместилось несколько итераций (192 моп в HSW IIRC). (т. е. «окно» инструкций, которое видит выполнение вне порядка, включает в себя несколько итераций). Таким образом, ЦП может завершить финальное хранение 2 или 3 итерации назад, а также начать в начале текущей итерации. Это скрывает задержку независимых цепей, поэтому важна только пропускная способность. На ядре в порядке, это потребовало бы программной конвейеризации ...
Питер Кордес

... Хороший компилятор ARM должен сделать кое-что за вас, если вы напишите его с внутренними компонентами (или с синтаксисом собственного вектора GNU C для всего этого, как я должен был сделать в первую очередь). У меня нет никакого опыта в том, чтобы сделать это по-настоящему, поэтому вам, возможно, придется помассировать ваш цикл и, возможно, сделать ручную развертку / программную конвейеризацию в источнике, чтобы получить хороший ассемблер. (Существуют некоторые ядра ARM не по порядку, встречающиеся в более дорогих телефонах, но они не имеют такого большого окна не по порядку, как у Haswell. OTOH, они также имеют более низкую пиковую пропускную способность, поэтому меньше получить от нахождения большего количества ILP).
Питер Кордес

1
@NominalAnimal: также согласился с глупостью вопроса. «Быстрее» без ограничений по качеству случайности глупо ... С BTRFS одни и те же данные на диске могут быть частью файла несколько раз (см. EXTENT_SAME в 4.2 ). Таким образом, вы можете сгенерировать случайный 4KB или 1MB и повторить его. Это случайность с коротким периодом, но она все еще случайна и стоит только ввод / вывод метаданных. (На самом деле, вам нужно, чтобы блок заканчивался символом новой строки. 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
правильное число для половины гигабайта: 1024 * 1024 * 1024/2 =536870912
Оливье Дюлак

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

6
@ gardenhead, все больше и больше людей сейчас склонны переходить на Gigabyte == 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-битной платформе каждый раз будет читаться 9 цифр вместо 19.


Это может вызвать исключение, если ваша система не поддерживает 64-разрядное целое число или perlне скомпилирована с поддержкой четырех типов.
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 цифры на входной байт. Если вы больше похожи на Стефана <16e18и делите его на 16, вы получите 18 цифр 86,7% при 1,95 дБ. С 32-битным, <4e9 /4получает 9 цифр 93,1% для 2,10 дБ. Но 5 байтов (в виде гексагона (H10)) <1e12дают 12 цифр 90,9% для 2,18 дпБ, или разделение гекса на половину и выполнение каждой половины <1e6 дает 6 цифр 95,4% для 2,29 дпБ; это приближается к пределу 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: Ну не совсем. Эта лапка имеет твердотельный накопитель Samsung 128 ГБ с последовательным чтением и записью ~ 500 МБ / с. Поместите четыре в конфигурацию Linux-Software-RAID0, и вы получите намного больше гигабайта, считывая и записывая при создании таких больших наборов данных (я оцениваю ~ 1,75 ТБ / с). 1 ГБ / с было достигнуто несколько лет назад с помощью 12 дисков SATA (вращающихся дисков, даже не SSD) с Linux sw-RAID0. (Примечание: байты / с, а не биты / с.) Конечно, это звучит глупо для «нормальной» машины, но те, кто играет с большими наборами данных, находят это стоящим - вы экономите время на всем, что делаете (с большими наборами данных) туда.
Номинальное животное

1
@NominalAnimal и Lu'u: Что еще более важно, если у вас достаточно оперативной памяти, программа может завершиться задолго до того, как все данные будут на диске. Большая часть работы в большом write()системном вызове - это memcpy в pagecache, который блокируется, только если ядро ​​решит сделать это вместо того, чтобы выделять больше буферного пространства. Эта программа должна быть узким местом на дисковых операциях ввода / вывода, когда не хватает памяти или если вы использовали O_DIRECT для обхода кэша страниц. Если вы write()занимаетесь кусками меньше размера кеша, надеюсь, ваши данные попадают в основную память только один раз, а перезаписываемый буфер остается горячим в кэш-памяти L2 или L3.
Питер Кордес

1

Это зависит от вашего определения «случайный». Если вы имеете в виду криптографически случайный, вам просто нужно взять хорошую библиотеку и откусить пулю, дождитесь ее запуска.

Если вам просто нужно что-то, что выглядит довольно случайно, вот простой способ:

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

На медленной машине может потребоваться час; достаточно быстро и достаточно случайно для большинства целей.


9
/dev/urandomскорее всего будет лучше gzip, как по скорости, так и по случайности.
Стиг Хеммер

Get a file that is several Gb longвам понадобится файл ** по крайней мере 8 ГБ, чтобы получить файл размером 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
Добро пожаловать на сайт! Смотрите ссылки на странице моего профиля. Здесь очень много проблем, которые я почти всегда вижу в сценариях оболочки, но это не делает их правильными.
Подстановочный

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, но имеет значение для Сэма, odнапример.
Стефан Шазелас

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