我尝试了一个 bash 脚本,但创建一个简单的 1 MB 文件花了太长时间。我认为答案在于使用/dev/random
or /dev/urandom
,但这里的其他帖子只展示了如何使用这些东西将各种数据添加到文件中,但我只想添加数字。
那么,是否有一个命令可以用来创建一个大小为 1 GB 的随机文件,其中只包含 0 到 9 之间的数字?
编辑:我希望输出是这样的
0 1 4 7 ..... 9
8 7 5 8 ..... 8
....
....
8 7 5 3 ..... 3
Run Code Online (Sandbox Code Playgroud)
范围是 0 - 9 意味着只有数字 0、1、2、3、4、5、6、7、8 和 9。我还需要它们以空格分隔,每行 100,最多行n
数。这 n 是我不在乎的东西,我希望我的最终大小为 1 GB。
编辑:我使用的是 Ubuntu 16.04 LTS
Sté*_*las 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
Run Code Online (Sandbox Code Playgroud)
(假设head
支持的实现-c
)在我的系统上似乎相当快。
tr
转换整个字节范围(0 到 255,0 到 0377 八进制):前 25 个字节为 0,接下来的 25 个字节为 1...然后 25 9 其余的(250 到 255)为“x”,然后我们丢弃 (with 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字节,而不是这 200 字节行中的 5000 万(head -n 50000000
也将使其成为标准/便携式命令)。
这些时间(zsh
在四核系统上获得)给出了 CPU 时间花费在哪里的指示:
LC_ALL=C tr '\0-\377' < /dev/urandom 0.61s user 31.28s system 99% cpu 31.904 total
tr -d x 1.00s user 0.27s system 3% cpu 31.903 total
fold -w 1 14.93s user 0.48s system 48% cpu 31.902 total
paste -sd "$(printf '%99s\\n')" - 7.23s user 0.08s system 22% cpu 31.899 total
head -c1G > /dev/null 0.49s user 1.21s system 5% cpu 31.898 total
Run Code Online (Sandbox Code Playgroud)
首先tr
是瓶颈,大部分时间都花在内核中(我想是用于随机数生成)。时间大致符合我可以从中获取字节的速率/dev/uramdom
(大约 19MiB/s,这里我们以 32MiB/s 的速率为 /dev/urandom 的每个 0.97 字节生成 2 个字节)。fold
似乎花费了不合理的 CPU 时间(15 秒)只是为了在每个字节后插入一个换行符,但这不会影响总时间,因为它在我的情况下在不同的 CPU 上工作(添加该-b
选项使它稍微多一点高效,dd cbs=1 conv=unblock
似乎是更好的选择)。
您可以head -c1G
通过在子shell 中设置文件大小限制(limit filesize 1024m
使用zsh
或ulimit -f "$((1024*1024))"
使用大多数其他shell(包括zsh
))来取消并缩短几秒钟。
如果我们为每个字节提取 2 位数字,这可以得到改善,但我们需要一种不同的方法。以上是非常有效的,因为tr
只需在 256 字节数组中查找每个字节。它不能一次执行 2 个字节,并且使用类似的东西hexdump -e '1/1 "%02u"'
使用更复杂的算法计算字节的文本表示将比随机数生成本身更昂贵。尽管如此,如果像我的情况一样,您有空闲的 CPU 内核,它仍然可以设法减少几秒钟:
和:
< /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
Run Code Online (Sandbox Code Playgroud)
我得到(但请注意,这里是 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
Run Code Online (Sandbox Code Playgroud)
总体 CPU 时间更长,但更好地分布在我的 4 个 CPU 内核之间,因此最终占用的挂钟时间更少。瓶颈是现在hexdump
。
如果我们使用dd
而不是 line-based fold
,我们实际上可以减少hexdump
需要做的工作量并提高 CPU 之间的工作平衡:
< /dev/urandom LC_ALL=C tr '\0-\377' '\0-\143\0-\143[x*]' |
tr -d x |
hexdump -ve '"%02u"' |
dd bs=50000 count=10000 iflag=fullblock status=none cbs=1 conv=unblock |
paste -sd "$(printf '%99s\\n')" -
Run Code Online (Sandbox Code Playgroud)
(这里假设 GNUdd
为它的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
Run Code Online (Sandbox Code Playgroud)
回到随机数生成是瓶颈。
现在,正如@OleTange 指出的那样,如果您拥有该openssl
实用程序,则可以使用它来获得更快的(尤其是在具有 AES 指令的处理器上)字节的伪随机生成器。
</dev/zero openssl enc -aes-128-ctr -nosalt -pass file:/dev/urandom
Run Code Online (Sandbox Code Playgroud)
在我的系统上每秒喷出的字节数是/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')" -
Run Code Online (Sandbox Code Playgroud)
现在给出:
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
Run Code Online (Sandbox Code Playgroud)
回到hexdump
瓶颈。
由于我还有 CPU 可用,我可以hexdump
并行运行其中的 3 个。
</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')" -
Run Code Online (Sandbox Code Playgroud)
(在后台运行时,除 /dev/null 上的关闭命令的 stdin<&3
之外的 shell 都需要zsh
它)。
现在下降到 6.2 秒,我的 CPU 几乎完全使用。
Nom*_*mal 40
由于问题的标题,这部分是半开玩笑的回答。
当您寻找“最快的方式...”时,答案几乎总是一些专门的工具。这个“答案”显示了一个这样的工具,只是为了让你可以试验。
这不是一个严肃的答案,因为你不应该为你只做一次或很少做的工作寻找专门的工具。你看,你最终会花更多的时间寻找工具并学习它们,而不是实际做事。Shell 和实用程序喜欢bash
和awk
不是最快的,但您通常可以编写一个单行代码来完成这项工作,只需花费几秒钟。perl
也可以使用更好的脚本语言,例如,虽然学习曲线perl
很陡峭,但我犹豫是否出于此类目的推荐它,因为我已经被糟糕的 perl 项目所伤害。python
另一方面,由于其相当慢的 I/O 而略有阻碍;不过,这只是您过滤或生成千兆字节数据时的问题。
在任何情况下,以下 C89 示例程序(仅在可用时才使用 POSIX.1 以获得更高精度的时钟)应达到大约 100 MB/s 的生成速率(在配备 Intel i5-4200U 处理器的笔记本电脑上的 Linux 中测试,管道输出to /dev/null
),使用一个很好的伪随机数生成器。(输出应通过所有 BigCrunch 测试,MatrixRank 测试除外,因为代码使用xorshift64*和排除方法来避免数字偏差。)
#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;
}
Run Code Online (Sandbox Code Playgroud)
如果我们切换到行缓冲区,我们可以使它更快,并且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;
}
Run Code Online (Sandbox Code Playgroud)
注意:两个示例都在 2016-11-18 上进行了编辑,以确保数字的均匀分布(不包括零;有关各种伪随机数生成器的比较和详细信息,请参见例如此处)。
编译使用例如
gcc -Wall -O2 decimal-digits.c -o decimal-digits
Run Code Online (Sandbox Code Playgroud)
并可选择在系统范围内安装以/usr/bin
使用
sudo install -o root -g root -m 0755 decimal-digits /usr/bin
Run Code Online (Sandbox Code Playgroud)
它需要每行的位数和行数。因为1000000000 / 100 / 2 = 5000000
(五百万;总字节数除以列除以 2),您可以使用
./decimal-digits 100 5000000 > digits.txt
Run Code Online (Sandbox Code Playgroud)
生成digits.txt
OP 所需的千兆字节大小。
请注意,程序本身的编写更注重可读性而不是效率。我在这里的目的不是为了展示代码的效率——无论如何我会使用 POSIX.1 和低级 I/O,而不是通用的 C 接口——而是让你很容易地看到什么样的平衡与付出的努力与单行程序或短 shell 或 awk 脚本相比,在开发专用工具及其性能方面。
使用 GNU C 库,fputc()
为每个字符输出调用函数会产生非常小的开销(间接函数调用或条件 -FILE
接口实际上非常复杂和通用,你看)。在这个特定的英特尔酷睿 i5-4200U 笔记本电脑上,将输出重定向到/dev/null
,第一个 (fputc) 版本大约需要 11 秒,而一次一行版本只需要 1.3 秒。
我碰巧经常编写这样的程序和生成器只是因为我喜欢玩庞大的数据集。我就是这么奇怪。例如,我曾经编写了一个程序,将所有有限的正 IEEE-754 浮点值打印到一个文本文件中,其精度足以在解析时产生完全相同的值。该文件有几 GB 大小(可能有 4G 左右);没有float
想象中那么多的有限正数。我用它来比较读取和解析此类数据的实现。
对于正常用例,就像 OP 一样,shell 脚本和 scriptlet 以及单行程序是更好的方法。完成整体任务所花费的时间更少。(除非他们每天左右需要一个不同的文件,或者有很多人需要一个不同的文件,在这种情况下 - 很少 - 像上面这样的专用工具,可能值得付出努力。)
Dig*_*uma 23
如果您有shuf
可用的(最近的 GNU coreutils 可用),您可以执行以下操作:
time shuf -r -n $((512*1024*1024)) -i 0-9 | paste -sd "$(printf '%99s\\n')" -
Run Code Online (Sandbox Code Playgroud)
在我的 VM 上,这现在比 Stéphane 的答案慢了大约 3:4。
Pet*_*des 21
如果您不需要非常高质量的随机性,并且接近均匀的分布就足够了,那么您可以非常快,尤其是在具有高效 SIMD 整数向量的现代 CPU 上,例如带有 SSE2 或 AVX2 的 x86。
这就像@NominalAnimal 的答案,因为我们都有相同的想法,但针对 x86 手动矢量化。(并且随机数质量较差,但对于许多用例来说仍然可能足够好。)这比@Nominal 的代码快 15 或 30 倍,在 2.5GHz Intel Haswell 上以 ~13GB/s 的 ASCII 输出带有 AVX2 的 CPU。这仍然低于理论最大主内存带宽(双通道 DDR3-1600 约为 25.6GB/s),但我正在定时写入 /dev/null,因此它实际上只是重写一个在缓存中保持热状态的缓冲区。Skylake 运行相同代码的速度应该比 Haswell 快得多(请参阅此答案的底部)。
假设您实际上在 I/O 到磁盘或在某处管道上存在瓶颈,快速实现意味着您的 CPU 甚至不必高于空闲时钟。它使用更少的总能量来产生结果。(电池寿命/热量/全球变暖。)
这太快了,您可能不想将其写入磁盘。只需根据需要重新生成(如果您再次想要相同的数据,则从相同的种子中生成)。即使您想将它提供给可以使用所有 CPU 的多线程进程,运行它以将数据通过管道传输到它也会使其在 L3 缓存(以及写入它的内核上的 L2 缓存)中保持热状态,并且非常使用CPU时间很少。(但要注意管道增加了大量的开销与写作的到/dev/null
。在一个SKYLAKE微架构i7-6700k,对管道wc -c
或其他程序,仅仅读+丢弃它的输入,它是关于8倍慢于写/dev/null
,只使用70% CPU。但在 3.9GHz CPU 上这仍然是 4.0GB/s。
即使从快速连接 PCIe 的 SSD 重新生成它也比重新读取它更快,但是 IDK 如果它更节能(矢量整数乘法器一直很忙,它可能非常耗电,还有其他 AVX2 256b 向量 ALU)。OTOH,我不知道从磁盘读取的 CPU 时间会占用多少处理此输入的所有内核的时间。我猜想以 128k 块重新生成的上下文切换可能会与运行文件系统/页面缓存代码和分配页面以从磁盘读取数据相竞争。当然,如果在pagecache中已经很火了,那基本上就是memcpy了。OTOH,我们已经写得和 memcpy 一样快了!(它必须在读取和写入之间分配主内存带宽)。(另请注意,写入内存'rep movsb
(优化微代码中的 memcpy 和 memset,避免了 RFO,因为 Andy Glew 在 P6 (Pentium Pro) 中实现了它))。
到目前为止,这只是一个概念证明,换行处理只是大致正确。在 2 的幂缓冲区的末端附近是错误的。随着更多的开发时间。我相信我可以找到一种更有效的方法来插入同样完全正确的换行符,开销至少与此一样低(与仅输出空格相比)。我认为这大约是 10% 到 20%。我只是想知道我们可以多快地运行,而不是实际拥有它的完善版本,所以我将把这部分留给读者作为练习,并附有描述一些想法的评论。
在具有 2.5GHz 最大睿频和 DDR3-1600MHz RAM 的 Haswell i5 上,定时产生 100GiB 但按比例缩小。(在带有 gcc5.4 的 Win10 上的 cygwin64 上计时-O3 -march=native
,-funroll-loops
由于我有足够的时间在这台借来的笔记本电脑上获得合适的计时,因此省略。应该刚刚在 USB 上启动 Linux)。
除非另有说明,否则写入 /dev/null。
wc -c
,具有 128kiB 缓冲区大小:0.32s,CPU 频率为 2.38GHz(最大双核涡轮增压)。(未缩放的时间:real=32.466s user=11.468s sys=41.092s,包括 this 和wc
)。但是,实际上只有一半的数据被复制,因为我愚蠢的程序假设 write 完成了整个缓冲区,即使情况并非如此,并且 cygwin write() 每次调用管道时只执行 64k。因此,使用 SSE2,这比@Nominal Animal 的标量代码快 15 倍。使用 AVX2,速度提高约 30 倍。我没有尝试只使用write()
代替的 Nominal 代码版本fwrite()
,但大概对于大缓冲区,stdio 大多不碍事。如果它正在复制数据,那将导致很多减速。
在 Core2Duo E6600(Merom 2.4GHz、32kiB 私有 L1、4MiB 共享 L2 缓存)、DDR2-533MHz 64 位 Linux 4.2 (Ubuntu 15.10)上生成 1GB 数据的时间。仍然为 write() 使用 128kiB 缓冲区大小,尚未探索该维度。
除非另有说明,否则写入 /dev/null。
wc -c
: 0.593s (unscaled: real=59.266s user=20.148s sys=1m6.548s,包括wc的CPU时间)。与 cygwin 相同数量的 write() 系统调用,但实际上是管道所有数据,因为 Linux 将 write() 的所有 128k 处理到管道。fwrite()
版本 (gcc5.2 -O3 -march=native
),运行时间./decdig 100 $((1024*1024*1024/200)) > /dev/null
:3.19s +/- 0.1%,每周期 1.40 条指令。-funroll-loops 可能产生了微小的差异。 clang-3.8 -O3 -march=native
: 3.42 秒 +/- 0.1%fwrite
piping to wc -c
: real=3.980s user=3.176s sys=2.080sclang++-3.8 -O3 -march=native
):22.885s +/- 0.07%,每个周期有 0.84 条指令。(g++5.2 稍慢:22.98s)。一次只写一行可能会造成很大的伤害。tr < /dev/urandom | ...
:real=41.430s user=26.832s sys=40.120s。 tr
大部分时间都将所有 CPU 内核都用于自身,几乎所有时间都花在内核驱动程序上,生成随机字节并将它们复制到管道中。这台双核机器上的另一个核心正在运行管道的其余部分。time LC_ALL=C head -c512M </dev/urandom >/dev/null
:即只是在没有管道的情况下读取那么多随机性:real=35.018s user=0.036s sys=34.940s。LANG=en_CA.UTF-8
::real=4m32.634s user=4m3.288s sys=0m29.364。LC_ALL=C LANG=C
: 真实=4m18.637s 用户=3m50.324s 系统=0m29.356s。还是很慢。dig3 = v%10
步骤在此硬件上大约是收支平衡):0.166s(每个周期 1.82 条指令) . 这基本上是我们可以通过完美高效的换行处理接近的下限。v%10
,0.222 秒+/- 0.4%,每个周期 2.12 条指令。(用 gcc5.2 编译,-march=native -O3 -funroll-loops
. 展开循环确实有助于此硬件上的此代码。不要盲目使用它,尤其是对于大型程序)。一个快速的 PRNG 显然是必不可少的。 xorshift128+可以矢量化,因此您可以在 SIMD 向量的元素中并行使用两个或四个 64 位生成器。每一步都会产生一个完整的随机字节向量。(此处使用英特尔内在函数实现 256b AVX2)。我选择了 Nominal 选择的 xorshift*,因为 64 位向量整数乘法只能在使用扩展精度技术的 SSE2/AVX2 中实现。
给定一个随机字节向量,我们可以将每个 16 位元素分成多个十进制数字。我们生成多个 16 位元素的向量,每个向量都是一个 ASCII 数字 + ASCII 空格。我们将其直接存储到我们的输出缓冲区中。
我的原始版本只是用来x / 6554
从向量的每个 uint16_t 元素中获取一个随机数字。它总是在 0 到 9 之间,包括 0 和 9。它偏离9
,因为(2^16 -1 ) / 6554
它只有 9.99923。(6554 = ceil((2^16-1)/10),这确保商总是 < 10。)
x/6554
可以通过乘以“神奇”常数(定点倒数)和高半结果的右移来计算。这是除以常数的最佳情况;一些除数需要更多的操作,签名除法需要额外的工作。 x % 10
具有类似的偏差,并且计算起来并不便宜。(gcc 的 asm 输出等效于x - 10*(x/10)
,即使用模乘法逆运算在除法之上进行额外的乘法和减法。)此外,xorshift128+ 的最低位质量不高,因此从高位进行除法以获取熵更好(质量和速度),而不是取模从低位取熵。
但是,我们可以通过查看低十进制数字(如@Nominal 的digit()
函数)来使用每个 uint16_t 中的更多熵。为了获得最佳性能,我决定采用低 3 位十进制数字和x/6554
,以节省一个 PMULLW 和 PSUBW(可能还有一些 MOVDQA),而采用 4 位低位十进制数字的更高质量选项。x/6554 受低 3 位十进制数字的影响很小,因此来自同一元素的数字之间存在一些相关性(ASCII 输出中的 8 或 16 位数字分隔,取决于向量宽度)。
我认为 gcc 是除以 100 和 1000,而不是一个较长的链,它连续除以 10,所以它可能不会显着缩短非循环携带的依赖链的长度,该链从每个 PRNG 输出产生 4 个结果。由于模乘逆和 xorshift+ 中的移位,port0(向量乘法和移位)是瓶颈,因此保存向量乘法绝对有用。
xorshift+ 是如此之快,以至于即使每 16 位仅使用约 3.3 位的随机性(即 20% 的效率)也不会比将其分成多个十进制数字慢很多。我们只近似均匀分布,因为只要质量不太差,这个答案就集中在速度上。
任何保持可变数量元素的条件行为都需要做更多的工作。(但也许仍然可以使用 SIMD 左打包技术在一定程度上有效地完成。但是,对于小元素尺寸,这会降低效率;巨大的 shuffle-mask 查找表不可行,并且没有小于 32- 的 AVX2 车道交叉洗牌位元素。128b PSHUFB 版本可能仍然能够使用 BMI2 PEXT/PDEP 动态生成掩码,就像使用较大元素的 AVX2 一样,但它很棘手,因为 64 位整数仅包含 8 个字节。godbolt 链接在那个答案上有一些代码可能适用于更高的元素计数。)
如果 RNG 的延迟是一个瓶颈,我们可以通过并行运行两个生成器向量来更快,交替使用我们使用的一个。编译器仍然可以轻松地将所有内容保存在展开循环中的寄存器中,这让两个依赖链并行运行。
在当前版本中,切断 PRNG 的输出,我们实际上是端口 0 吞吐量的瓶颈,而不是 PRNG 延迟,因此没有必要。
完整版,在 Godbolt 编译器资源管理器中有更多评论。
不是很整洁,抱歉我要睡觉了,想把这个贴出来。
要获取 SSE2 版本,将s/_mm256/_mm
、s/256/128/
、s/v16u/v8u/
、 和更改vector_size(32)
为 16。同时将换行增量从 4*16 更改为 4*8。(就像我说的,代码很乱,没有很好地设置来编译两个版本。最初没有计划制作 AVX2 版本,但后来我真的很想在我可以访问的 Haswell CPU 上进行测试。)
#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):
// /sf/ask/1680135131/
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);
}
}
Run Code Online (Sandbox Code Playgroud)
使用 gcc、clang 或 ICC(或希望理解 C99 的 GNU C 方言和 Intel 的内在函数的任何其他编译器)进行编译。GNU C 向量扩展非常方便让编译器使用模乘法逆来生成除法/模数的幻数,偶尔使用__attribute__
s 很有用。
这可以移植,但需要更多的代码。
用于插入换行符的重叠存储在决定将其放置在哪里时有很大的开销(分支预测错误和 Core2 上的前端瓶颈),但存储本身对性能没有影响。仅在编译器的 asm 中注释掉该存储指令(保留所有分支相同),Core2 上的性能完全不变,重复运行使相同的时间达到 +/- 小于 1%。所以我得出结论,存储缓冲区/缓存处理得很好。
尽管如此,ascii_digitspace
如果我们展开足够多的计数器/分支消失,使用某种带有换行符的元素的旋转窗口可能会更快。
写入 /dev/null 基本上是一个空操作,因此缓冲区可能在 L2 缓存中保持热状态(Haswell 上每个内核 256kiB)。从 128b 向量到 256b 向量的完美加速是预期的:没有额外的指令,并且所有内容(包括存储)都以两倍的宽度发生。但是,换行插入分支的使用频率是原来的两倍。不幸的是,我没有在我的 Haswell cygwin 设置上花时间把那部分#ifdef
删掉。
2.5GHz * 32B / 13.7GB/s = 5.84 个周期,每个 AVX2 存储在 Haswell 上。 这很好,但可能会更快。也许 cygwin 系统调用中的开销比我想象的要大。我没有尝试在编译器的 asm 输出中注释掉那些(这将确保没有优化掉。)
L1 缓存每个时钟可以维持一个 32B 存储,而 L2 的带宽并不低很多(但延迟更高)。
当我在几个版本前查看 IACA 时(没有换行的分支,但每个 RNG 向量只得到一个 ASCII 向量),它预测了每 4 或 5 个时钟有一个 32B 向量存储。
我希望通过从每个 RNG 结果中提取更多数据来获得更多的加速,基于我自己查看 asm,考虑到Agner Fog 的指南和其他优化资源,我在SO x86 标签 wiki 中添加了链接。)
可能它在 Skylake 上会明显更快,其中向量整数乘法和移位可以在两倍于 Haswell(仅 p0)的端口(p0 / p1)上运行。xorshift 和数字提取都使用了大量的移位和乘法。(更新:Skylake 以 3.02 IPC 运行它,每个 32 字节 AVX2 存储为我们提供 3.77 个周期,每 1GB 迭代计时 0.030 秒,/dev/null
在 i7-6700k 上以 3.9GHz写入Linux 4.15。
它不需要 64 位模式才能正常工作。使用 编译时-m32
,SSE2 版本同样快,因为它不需要很多向量寄存器,并且所有 64 位数学运算都是在向量中完成的,而不是通用寄存器。
它实际上在 Core2 上的 32 位模式下稍微快一些,因为比较/分支宏融合仅在 32 位模式下有效,因此无序内核的 uops 更少(18.3s(每时钟 1.85 条指令)与. 16.9 秒(2.0 IPC))。没有 REX 前缀的较小代码大小也有助于 Core2 的解码器。
此外,一些 reg-reg 向量移动被负载替换,因为并非所有常量都固定在向量 regs
sam*_*var 14
这是一个我希望很容易理解的解决方案:
od -An -x /dev/urandom | tr -dc 0-9 | fold -w100 | awk NF=NF FS= | head -c1G
Run Code Online (Sandbox Code Playgroud)
od
从/dev/random
.tr
去掉字母,只保留0-9
数字fold
确保每行有 100 位数字awk
在行内插入空格head
将输入截断为 1 GB您可以jot
为此使用以下命令:
jot -r 50000000 0 9 | fmt -w 200 > output.txt
Run Code Online (Sandbox Code Playgroud)
这类似于 Stéphane Chazelas 的方法,但是我一次读取 64 位以提高性能。分布仍然是统一的,但现在每 8 个字节得到 19 个数字,而不是像以前那样在最好的情况下只有 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
Run Code Online (Sandbox Code Playgroud)
在 32 位平台上,每次读取 9 位而不是 19 位。
归档时间: |
|
查看次数: |
20663 次 |
最近记录: |