读取每个30字节大二进制文件的最快方法?

读取大型二进制文件(2-3 GB)的每30个字节的最快方法是什么? 我已经读过由于I / O缓冲区而导致fseek出现性能问题,但我不想在抓取每30个字节之前将2-3 GB的数据读入内存。

性能测试。 如果您想自己使用它,请注意完整性检查(打印总计)仅在“step”划分BUFSZ时有效,并且MEGS足够小以至于您不读取文件的末尾。 这是由于(a)懒惰,(b)希望不掩盖真实的代码。 rand1.data是使用dd从/ dev / urandom复制的几GB。

 #include  #include  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.4s,将其带入缓存。 第二次(使用32字节缓冲区)需要0.17秒。 第三次(再次使用32KB缓冲区)需要0.03s,这太接近我的计时器的粒度才有意义。 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 

忽略这三个中的第一个,它从已经在RAM中的第一个1000MB文件中受益。

现在,32KB的版本在挂钟时间上稍微快一点(我不能再重新运行了,所以现在让我们忽略它),但是看看用户+系统时间的差异:20s vs. 82S。 我认为我的操作系统的推测预读磁盘缓存已经保存了32字节缓冲区的培根:当32字节缓冲区正在缓慢重新填充时,操作系统正在加载接下来的几个磁盘扇区,即使没有人要求它们。 没有它,我怀疑它会比32KB缓冲区慢一分钟(20%),在请求下一次读取之前,它在用户区中花费的时间更少。

故事的道德:标准的I / O缓冲并没有在我的实现中削减它,fseek的表现非常恶劣,正如提问者所说的那样。 当文件缓存在OS中时,缓冲区大小是一个大问题。 当文件没有缓存在操作系统中时,缓冲区大小与挂钟时间没有太大差别,但我的CPU比较繁忙。

incrediman使用读缓冲区的基本建议至关重要,因为fseek令人震惊。 争论缓冲区应该是几KB还是几百KB在我的机器上很可能毫无意义,可能是因为操作系统已经完成了确保操作严格受I / O限制的工作。 但我很确定这是由OS磁盘预读而不是标准I / O缓冲,因为如果是后者那么fseek会比它更好。 实际上,可能是标准I / O正在进行预读,但是fseek的过于简单的实现每次都会丢弃缓冲区。 我没有查看实现(如果我这样做,我无法跨越边界进入操作系统和文件系统驱动程序)。

我建议你创建一个几千字节的缓冲区,从它每30个字节读取一次,用接下来的几千字节重新加载缓冲区,然后继续直到你到达eof。 这样,读入内存的数据量就会受到限制,您也不必经常从文件中读取数据。 你会发现你创建的缓冲区越大,它就越快。

编辑:实际上,如下所示,你可能想让你的缓冲区几百kb,而不是几千字节(就像我说的 – 更大的缓冲区=更快的文件读取)。

好吧,你可以读取一个字节,然后在循环中寻找29个字节。 但IO子系统必须按扇区读取文件,这些扇区的大小通常为512字节,因此它仍将最终读取整个文件。

从长远来看,以块大小的倍数读取整个文件会更快,然后只需查看缓冲区。 如果确保缓冲区大小是30的倍数,那么你的生活会变得更简单,如果它是512的倍数,你可以让文件子系统的生命更轻松。

 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上运行,并且愿意特定于操作系统,那么您实际上无法击败内存映射文件的性能。 如何扫描磁盘上真正巨大的文件?

如果您愿意打破ANSI-C并使用特定于OS的调用,我建议使用内存映射文件。 这是Posix版本(Windows有自己的OS特定调用):

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

缓冲I / O库的全部目的是让您免于此类担忧。 如果你必须每30个字节读一次,操作系统将最终读取整个文件,因为操作系统会读取更大的块。 以下是您的选择,从最高性能到最低性能:

  • 如果您有一个大的地址空间(即,您在64位硬件上运行64位操作系统),那么使用内存映射IO(POSIX系统上的mmap )将节省您从中获取操作系统复制数据的成本内核空间到用户空间。 这种节省可能很大。

  • 如下面的详细说明(感谢Steve Jessop的基准测试),如果您关心I / O性能,您应该从AT&T高级软件技术组下载Phong Vo的sfio库 。 它比C的标准I / O库更安全,设计更好,速度更快。 在使用fseek的程序中,速度要快得多:在简单的微基准测试上快7倍。

  • 只需放松并使用fseekfgetc ,它们的设计和实施完全可以解决您的问题。

如果你认真对待这个问题,你应该测量所有三种选择 。 Steve Jessop和我表明使用fseek比较慢,如果你使用的是GNU C库, fseek会慢得多。 你应该测量mmap ; 它可能是最快的。


附录:您希望查看文件系统并确保它可以快速从磁盘中提取2-3 GB。 例如,XFS可能击败ext2。 当然,如果你遇到NTFS或HFS +,它就会变慢。

令人震惊的结果只是在

我在Linux上重复了Steve Jessop的测量。 GNU C库在每个fseek进行系统调用 。 除非POSIX出于某种原因需要这个,否则它是疯了。 我可以咀嚼一堆零和零,然后呕吐一个更好的缓冲I / O库。 无论如何,成本上升了大约20倍,其中大部分花费在内核中。 如果使用fgetc而不是fread来读取单个字节,则可以在小基准测试中节省大约20%。

使用体面的I / O库可以减少令人震惊的结果

我再次做了实验,这次是使用Phong Vo的sfio库。 读取200MB需要

  • 不使用fseek BUFSZBUFSZ为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和fread函数的调用开销。 您一次读取的块越大,您节省的通话费用就越多 – 尽管其他成本显然会让您感觉超出某一点。

如果您正在使用旋转盘片从硬盘读取数据,答案是您使用大缓冲区顺序读取整个文件并丢弃内存中您不想要的部分。

可以使用标准硬盘驱动器的最小访问单元是扇区。 所有常见旋转磁盘驱动器的扇区大小都超过30个字节。 这意味着硬盘控制器必须无论如何都要访问每个扇区,无论主机的请求是什么样的。 没有低级魔法可以改变这一点。

即使不是这种情况,并且您可以读取单个字节,但搜索与顺序读取操作相比有很大的优势。 最好的情况仍然与顺序读取相同。 在现实世界中,如果信令开销会阻止这些方案即使使用大量命令缓冲区,也不会感到惊讶。