读取大型二进制文件每 30 个字节的最快方法?
Posted
技术标签:
【中文标题】读取大型二进制文件每 30 个字节的最快方法?【英文标题】:Fastest way to read every 30th byte of large binary file? 【发布时间】:2011-01-24 13:37:31 【问题描述】:读取大型二进制文件 (2-3 GB) 的每 30 个字节的最快方法是什么?我读到 fseek 存在由于 I/O 缓冲区的性能问题,但我也不想在每 30 个字节抓取之前将 2-3 GB 的数据读入内存。
【问题讨论】:
【参考方案1】:我的建议是创建一个几千字节的缓冲区,从中读取每 30 个字节,用接下来的几千字节重新加载缓冲区,然后继续直到到达 eof。这样,读入内存的数据量是有限的,您也不必经常从文件中读取。你会发现你创建的缓冲区越大,速度就越快。
编辑:实际上,如下所示,您可能希望将缓冲区设置为几百 kb,而不是几千字节(就像我说的 - 更大的缓冲区 = 更快的文件读取)。
【讨论】:
+1 -- 只是写几乎完全相同的东西 -- 除了我建议每块几百千字节。 是的,这可能更好。我的意思是,如果文件那么大,那么他显然处于一个可以承受大于几千字节的缓冲区的环境中:)(编辑后的答案) 我预测,与标准 I/O 库中使用的默认缓冲策略相比,这种方案的好处甚至无法衡量(对于每 30 个字节读取的程序)。我很高兴看到测量结果证明我错了。 @Norman Ramsey:我预测不是这样。正在测试中,我会尽快发布 CW 答案。 在许多平台上,使缓冲区大小/读取大小与磁盘的扇区大小相匹配会导致读取速度最快。【参考方案2】:好吧,您可以读取一个字节,然后在循环中查找 29 个字节。但是 IO 子系统必须按扇区读取文件,通常大小为 512 字节,因此它最终仍会读取整个文件。
从长远来看,以步长倍数的块读取整个文件,然后只查看缓冲区会更快。如果你确保缓冲区大小是 30 的倍数,你会让你的生活更简单,如果它是 512 的倍数,你会让 fileio 子系统的生活更轻松。
while (still more file to read)
char buf[30 * 512];
int cread = fread (buf, sizeof(buf), 1, fd);
for (int ii = 0; ii < cread; ii += 30)
这可能看起来效率低下,但它会比尝试读取 30 字节块更快。
顺便说一句。如果您在 Windows 上运行,并且愿意特定于操作系统,那么您真的无法击败内存映射文件的性能。 How to scan through really huge files on disk?
【讨论】:
重要的一点是扇区大小意味着操作系统无论如何都会读取整个文件。 当然,Windows 并不是唯一具有内存映射文件的平台。 @Ken:我对 mmap 相对于 fread 的执行情况没有第一手知识,我链接到的示例代码仅适用于 Windows。【参考方案3】:如果您愿意脱离 ANSI-C 并使用特定于操作系统的调用,我建议您使用内存映射文件。这是 Posix 版本(Windows 有自己的操作系统特定调用):
#define MAPSIZE 4096
int fd = open(file, O_RDONLY);
struct stat stbuf;
fstat(fd, &stbuf);
char *addr = 0;
off_t last_mapped_offset = -1;
off_t idx = 0;
while (idx < stbuf.st_size)
if (last_mapped_offset != (idx / MAPSIZE))
if (addr)
munmap(addr, MAPSIZE);
last_mapped_offset = idx / MAPSIZE;
addr = mmmap(0, MAPSIZE, PROT_READ, MAP_FILE, fd, idx, last_mapped_offset);
*(addr + (idx % MAPSIZE));
idx += 30;
munmap(addr, MAPSIZE);
close(fd);
【讨论】:
当您一次只mmap()
一页并且从不调用 madvise()
时,典型的基于 POSIX 的操作系统是否仍会执行预读?
顺便说一句,mmap()
使用SIGBUS
报告文件映射后发生的错误。这比来自read()
或fread()
的错误更难正确处理。【参考方案4】:
您几乎可以肯定不需要担心它。运行时可以很好地缓冲它为每个文件句柄读取的最后一个块。即使没有,操作系统也会为您缓存文件访问。
也就是说,如果您一次读取一个块,您确实可以节省 fseek 和 fread 函数的调用开销。您一次阅读的块越大,您节省的通话费用就越多 - 尽管其他成本显然开始让自己感觉超出了某个点。
【讨论】:
【参考方案5】:缓冲 I/O 库的整个目的就是让您摆脱这些顾虑。如果您必须每 30 个字节读取一次,那么操作系统将最终读取整个文件,因为操作系统会读取更大的块。以下是您的选项,从最高性能到最低性能:
如果您有较大的地址空间(即,您在 64 位硬件上运行 64 位操作系统),那么使用内存映射 IO(在 POSIX 系统上为mmap
)将为您节省让操作系统将数据从内核空间复制到用户空间的成本。这种节省可能非常可观。
如下面的详细说明所示(感谢 Steve Jessop 的基准测试),如果您关心 I/O 性能,您应该从 AT&T 高级软件技术组下载 Phong Vo 的sfio library。它比 C 的标准 I/O 库更安全、设计更好、速度更快。在经常使用fseek
的程序上,它显着更快:
在一个简单的微基准测试中,速度提高了七倍。
放松一下,使用fseek
和fgetc
,它们正是为解决您的问题而设计和实现的。
如果您认真对待这个问题,您应该衡量所有三个备选方案。 Steve Jessop 和我展示了使用 fseek
会更慢,如果您使用的是 GNU C 库,fseek
会慢很多。你应该测量mmap
;它可能是最快的。
附录:您希望查看您的文件系统,并确保它可以快速从磁盘中提取 2-3 GB。例如,XFS 可能会击败 ext2。当然,如果您坚持使用 NTFS 或 HFS+,它只会很慢。
令人震惊的结果就在
我重复了 Steve Jessop 在 Linux 上的测量。 GNU C 库在每个fseek
进行系统调用。除非 POSIX 出于某种原因需要这样做,否则这太疯狂了。我可以咀嚼一堆 1 和 0,然后 puke 比这更好的缓冲 I/O 库。无论如何,成本增加了大约 20 倍,其中大部分花费在内核中。如果您使用fgetc
而不是fread
来读取单个字节,您可以在小型基准测试中节省大约 20%。
使用不错的 I/O 库,结果不会那么令人震惊
我再次进行了实验,这次使用的是 Phong Vo 的 sfio
库。读取 200MB 需要
fseek
(BUFSZ
为 30k)
0.57s 使用fseek
重复测量表明,在没有fseek
的情况下,使用 sfio 仍然可以减少大约 10% 的运行时间,但运行时间非常嘈杂(几乎所有时间都花在了操作系统上)。
在这台机器(笔记本电脑)上,我没有足够的可用磁盘空间来运行不适合磁盘缓存的文件,但我愿意得出以下结论:
使用合理的 I/O 库,fseek
更昂贵,但不会更昂贵足够产生很大的不同(如果你所做的只是 I/O,则需要 4 秒)。
GNU 项目不提供合理的 I/O 库。通常情况下,GNU 软件很烂。
结论:如果你想要快速 I/O,你的第一步应该是用 AT&T sfio 库替换 GNU I/O 库。相比之下,其他影响可能很小。
【讨论】:
准备好震惊吧,fseek 会导致我的机器(NTFS、Windows XP、cygwin)大幅减速。 @Steve:我对 cygwin 持怀疑态度。我很想知道与 Microsoft C 编译器和库(相同代码)相比性能如何。 “我可以咀嚼一堆 1 和 0,然后吐出一个比这更好的缓冲 I/O 库。”它是开源的。自己重写并提交;如果它由于某些重要原因而被拒绝(例如 POSIX 需要它),那么您就会知道为什么 GNU 库表现如此糟糕。如果它被接受,那么您将单枪匹马地对 Linux 的默认 I/O 库进行巨大改进。【参考方案6】:性能测试。如果您想自己使用它,请注意完整性检查(打印总计)仅在“步”除 BUFSZ 并且 MEGS 足够小以至于您不会读取文件末尾时才有效。这是由于(a)懒惰,(b)不想掩盖真实代码。 rand1.data 是使用 dd
从 /dev/urandom 复制的几 GB。
#include <stdio.h>
#include <stdlib.h>
const long long size = 1024LL*1024*MEGS;
const int step = 32;
int main()
FILE *in = fopen("/cygdrive/c/rand1.data", "rb");
int total = 0;
#if SEEK
long long i = 0;
char buf[1];
while (i < size)
fread(buf, 1, 1, in);
total += (unsigned char) buf[0];
fseek(in, step - 1, SEEK_CUR);
i += step;
#endif
#ifdef BUFSZ
long long i = 0;
char buf[BUFSZ];
while (i < size)
fread(buf, BUFSZ, 1, in);
i += BUFSZ;
for (int j = 0; j < BUFSZ; j += step)
total += (unsigned char) buf[j];
#endif
printf("%d\n", total);
结果:
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=20 && time ./buff2
83595817
real 0m1.391s
user 0m0.030s
sys 0m0.030s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32 -DMEGS=20 && time ./buff2
83595817
real 0m0.172s
user 0m0.108s
sys 0m0.046s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=20 && time ./buff2
83595817
real 0m0.031s
user 0m0.030s
sys 0m0.015s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32 -DMEGS=20 && time ./buff2
83595817
real 0m0.141s
user 0m0.140s
sys 0m0.015s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DSEEK -DMEGS=20 && time ./buff2
83595817
real 0m20.797s
user 0m1.733s
sys 0m9.140s
总结:
我最初使用 20MB 的数据,这当然适合缓存。我第一次读取它(使用 32KB 缓冲区)需要 1.4 秒,将其放入缓存中。第二次(使用 32 字节缓冲区)需要 0.17 秒。第三次(再次返回 32KB 缓冲区)需要 0.03 秒,这太接近我的计时器的粒度而没有意义。 fseek 需要超过 20 秒,即使数据已经在磁盘缓存中。
此时我将 fseek 拉出环,以便其他两个可以继续:
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=1000 && time ./buff2
-117681741
real 0m33.437s
user 0m0.749s
sys 0m1.562s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32 -DMEGS=1000 && time ./buff2
-117681741
real 0m6.078s
user 0m5.030s
sys 0m0.484s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=1000 && time ./buff2
-117681741
real 0m1.141s
user 0m0.280s
sys 0m0.500s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32 -DMEGS=1000 && time ./buff2
-117681741
real 0m6.094s
user 0m4.968s
sys 0m0.640s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=1000 && time ./buff2
-117681741
real 0m1.140s
user 0m0.171s
sys 0m0.640s
1000MB 的数据似乎也被大量缓存。 32KB 缓冲区比 32 字节缓冲区快 6 倍。但区别在于所有用户时间,而不是阻塞在磁盘 I/O 上的时间。现在,8000MB 比我的 RAM 多得多,所以我可以避免缓存:
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=8000 && time ./buff2
-938074821
real 3m25.515s
user 0m5.155s
sys 0m12.640s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32 -DMEGS=8000 && time ./buff2
-938074821
real 3m59.015s
user 1m11.061s
sys 0m10.999s
$ gcc -std=c99 buff2.c -obuff2 -O3 -DBUFSZ=32*1024 -DMEGS=8000 && time ./buff2
-938074821
real 3m42.423s
user 0m5.577s
sys 0m14.484s
忽略这三个中的第一个,它受益于文件的前 1000MB 已经在 RAM 中。
现在,32KB 的版本在挂钟时间上只是稍微快了一点(我懒得重新运行,所以我们暂时忽略它),但看看用户+系统时间的差异: 20 多岁与 82 多岁。我认为我的操作系统的推测性预读磁盘缓存在这里保存了 32 字节缓冲区的培根:虽然 32 字节缓冲区正在缓慢重新填充,但操作系统正在加载接下来的几个磁盘扇区,即使没有人要求它们。如果没有它,我怀疑它会比 32KB 缓冲区慢一分钟 (20%),后者在请求下一次读取之前在用户空间中花费的时间更少。
故事的寓意:标准 I/O 缓冲在我的实现中并没有削减它,正如提问者所说,fseek 的性能很糟糕。当文件缓存在操作系统中时,缓冲区大小很重要。当文件未缓存在操作系统中时,缓冲区大小对挂钟时间没有太大影响,但我的 CPU 更忙。
incrediman 使用读取缓冲区的基本建议至关重要,因为 fseek 令人震惊。在我的机器上争论缓冲区应该是几 KB 还是几百 KB 很可能毫无意义,可能是因为操作系统已经完成了确保操作严格 I/O 绑定的工作。但我很确定这取决于 OS 磁盘预读,而不是标准 I/O 缓冲,因为如果是后者,那么 fseek 会比现在更好。实际上,可能是标准 I/O 正在执行预读,但是 fseek 的一个过于简单的实现是每次都丢弃缓冲区。我还没有研究过实现(如果我这样做了,我也无法跨越边界进入操作系统和文件系统驱动程序)。
【讨论】:
非常酷。但是fread
没有针对 1 个字符进行优化。你可以试试fgetc
吗?
fgetc 与 fread 没有区别,我可以在每个测试运行 4 次中检测到(MEGS=20,数据预加载)。结果范围是 19.4s 到 21.2s,最好和最差都使用 fgetc。我希望其他人的里程会有所不同——我不知道 cygwin+gcc 在多大程度上使用了未经修改的 glibc,而且我不知道 Windows 的某些特性是否会对 fseek 的性能造成影响。您会认为 31 字节的前向搜索“应该”在大多数情况下只增加 FILE* 中的偏移量,但显然不是。
我追踪了它;傻瓜对每个fseek
进行一次系统调用。什么白痴!我更改了您的程序以使用 Phong Vo 的 sfio 库,此时差异仍然存在,但它们相当小。感谢您发布如此有用的程序。哦,还有 +1 :-)
谢谢,诺曼。性能问题的第一条规则:编写一个半途而废的基准通常很容易,而半途而废的基准通常足以揭示严重的性能灾难:-)
Phong Vo 的 sfio 库可以在 github.com/ellson/graphviz/tree/master/lib/sfio 找到(以及其他地方,但这里的一些早期链接已断开)。【参考方案7】:
如果您从带有旋转盘片的硬盘读取数据,答案是您使用大缓冲区顺序读取整个文件,并丢弃内存中不需要的部分。
对标准硬盘驱动器的最小访问单位是扇区。所有常见的旋转磁盘驱动器的扇区大小都超过 30 字节。这意味着无论来自主机的请求是什么样的,硬盘控制器都必须访问每个扇区。没有低级魔法可以改变这一点。
即使情况并非如此,并且您可以读取单个字节,但查找操作与顺序读取操作相比还是有巨大的溢价。最好的情况仍然与顺序读取相同。在现实世界中,如果信令开销会阻止此类方案即使使用大量命令缓冲区也无法工作,我不会感到惊讶。
【讨论】:
以上是关于读取大型二进制文件每 30 个字节的最快方法?的主要内容,如果未能解决你的问题,请参考以下文章