在C语言中最快的文件读取方式

25

目前我正在使用fread()函数读取文件,但是有人告诉我在其他语言中fread()函数效率很低。在C语言中是否也是如此?如果是,那么如何更快地读取文件?


4
你指的是哪种“其他”语言? - luke
请参考我之前所提问的类似问题:https://dev59.com/IXRB5IYBdhLWcg3wEDyl - dreamlax
@dreamlax:我刚刚看了你的问题。但是,根据我的基准测试,fgets()在速度上与我手写的缓冲读取()相似。我正在测试一个有8.3M行的1.3GB文件。你介意发一下你的示例程序,展示缓冲读取()比fgets()快4倍吗?提前感谢。至于Jay的问题,我认为在大多数情况下,fread()与read()相似或更可能比read()更快。 - user172818
文本文件还是二进制文件?C运行时库接口可能会对换行符进行少量转换,如果是文本文件的话。如果您不需要这种转换,可以使用另一个API来绕过它。 - Adrian McCarthy
当时受到质疑的语言正是C++。 - Jay
8个回答

42

这并不重要。

如果你正在从实际硬盘中读取,速度会很慢。硬盘是瓶颈,就是这样。

现在,如果你对于调用read/fread/等函数有些草率,比如每次只读取一个字节,那么速度会很慢,因为fread()的开销将超过从磁盘中读取的开销。

如果你调用read/fread/等函数并请求一定数量的数据,这将取决于你所做的事情:有时候你只需要4个字节(获取uint32),但有时你可以读取大块数据(4 KiB、64 KiB等等。内存很便宜,选择一些重要的东西吧)。

如果你正在进行小型读取,一些高级别的调用,如fread(),将通过在后台缓冲数据来帮助你。如果你正在进行大型读取,它可能没有什么帮助,但是从fread切换到read可能不会产生太多改进,因为你的瓶颈在于磁盘速度。

简而言之:如果可以的话,在读取时请求充足的数据,并尽量减少你写入的内容。对于大量数据,2的幂值通常比其他任何值更友好,但当然,这取决于操作系统、硬件和环境。

所以,让我们看看这是否会带来任何不同:

#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

#define BUFFER_SIZE (1 * 1024 * 1024)
#define ITERATIONS (10 * 1024)

double now()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec / 1000000.;
}

int main()
{
    unsigned char buffer[BUFFER_SIZE]; // 1 MiB buffer

    double end_time;
    double total_time;
    int i, x, y;
    double start_time = now();

#ifdef USE_FREAD
    FILE *fp;
    fp = fopen("/dev/zero", "rb");
    for(i = 0; i < ITERATIONS; ++i)
    {
        fread(buffer, BUFFER_SIZE, 1, fp);
        for(x = 0; x < BUFFER_SIZE; x += 1024)
        {
            y += buffer[x];
        }
    }
    fclose(fp);
#elif USE_MMAP
    unsigned char *mmdata;
    int fd = open("/dev/zero", O_RDONLY);
    for(i = 0; i < ITERATIONS; ++i)
    {
        mmdata = mmap(NULL, BUFFER_SIZE, PROT_READ, MAP_PRIVATE, fd, i * BUFFER_SIZE);
        // But if we don't touch it, it won't be read...
        // I happen to know I have 4 KiB pages, YMMV
        for(x = 0; x < BUFFER_SIZE; x += 1024)
        {
            y += mmdata[x];
        }
        munmap(mmdata, BUFFER_SIZE);
    }
    close(fd);
#else
    int fd;
    fd = open("/dev/zero", O_RDONLY);
    for(i = 0; i < ITERATIONS; ++i)
    {
        read(fd, buffer, BUFFER_SIZE);
        for(x = 0; x < BUFFER_SIZE; x += 1024)
        {
            y += buffer[x];
        }
    }
    close(fd);

#endif

    end_time = now();
    total_time = end_time - start_time;

    printf("It took %f seconds to read 10 GiB. That's %f MiB/s.\n", total_time, ITERATIONS / total_time);

    return 0;
}

...产生:

$ gcc -o reading reading.c
$ ./reading ; ./reading ; ./reading 
It took 1.141995 seconds to read 10 GiB. That's 8966.764671 MiB/s.
It took 1.131412 seconds to read 10 GiB. That's 9050.637376 MiB/s.
It took 1.132440 seconds to read 10 GiB. That's 9042.420953 MiB/s.
$ gcc -o reading reading.c -DUSE_FREAD
$ ./reading ; ./reading ; ./reading 
It took 1.134837 seconds to read 10 GiB. That's 9023.322991 MiB/s.
It took 1.128971 seconds to read 10 GiB. That's 9070.207522 MiB/s.
It took 1.136845 seconds to read 10 GiB. That's 9007.383586 MiB/s.
$ gcc -o reading reading.c -DUSE_MMAP
$ ./reading ; ./reading ; ./reading 
It took 2.037207 seconds to read 10 GiB. That's 5026.489386 MiB/s.
It took 2.037060 seconds to read 10 GiB. That's 5026.852369 MiB/s.
It took 2.031698 seconds to read 10 GiB. That's 5040.119180 MiB/s.

...或者没有明显的区别。(有时fread会胜出,有时是read)

注意:慢的mmap令人惊讶。这可能是因为我要求它为我分配缓冲区。(我不确定是否需要提供指针...)

简而言之:不要过早优化。先让它运行起来,再让它正确,最后才让它快速,按照这个顺序。


应受欢迎的请求,我在真实文件上运行了测试。(Ubuntu 10.04 32位桌面安装CD ISO的前675 MiB)以下是结果:

# Using fread()
It took 31.363983 seconds to read 675 MiB. That's 21.521501 MiB/s.
It took 31.486195 seconds to read 675 MiB. That's 21.437967 MiB/s.
It took 31.509051 seconds to read 675 MiB. That's 21.422416 MiB/s.
It took 31.853389 seconds to read 675 MiB. That's 21.190838 MiB/s.
# Using read()
It took 33.052984 seconds to read 675 MiB. That's 20.421757 MiB/s.
It took 31.319416 seconds to read 675 MiB. That's 21.552126 MiB/s.
It took 39.453453 seconds to read 675 MiB. That's 17.108769 MiB/s.
It took 32.619912 seconds to read 675 MiB. That's 20.692882 MiB/s.
# Using mmap()
It took 31.897643 seconds to read 675 MiB. That's 21.161438 MiB/s.
It took 36.753138 seconds to read 675 MiB. That's 18.365779 MiB/s.
It took 36.175385 seconds to read 675 MiB. That's 18.659097 MiB/s.
It took 31.841998 seconds to read 675 MiB. That's 21.198419 MiB/s.

经过一位非常无聊的程序员的努力,我们成功从磁盘中读取了CD ISO文件,共进行了12次测试。在每次测试前,都清空了磁盘缓存,并且在每次测试期间,有足够的、大致相同数量的可用内存,可以将CD ISO文件存储两次。

值得注意的一点是,我最初使用了大型的malloc()函数来填充内存,以此来减少磁盘缓存的影响。可能值得一提的是,mmap表现十分糟糕。另外两种方法只是简单地运行,而mmap运行时,由于某些原因我无法解释,开始将内存推到swap区,导致性能下降。(据我所知(源代码在上面),程序没有泄漏,实际的“已使用内存”在试验过程中保持不变。)

总体上,read()方法的速度最快,fread()方法的时间非常稳定。然而,在测试期间可能会出现一些小问题。所有这三种方法基本上都是相等的。(特别是fread和read方法……)


3
9533.226368 MiB/s... 不知怎么的,我觉得你的代码有问题。这比内存通常运行速度还要快得多,更不用说硬盘了。 - Billy ONeal
3
这样做是为了让 mmap 实际读取它,否则它不会交换数据。这就是为什么 mmap 通常更快的原因。通常你不需要文件中的所有字节。内存映射在写入方面也更有帮助,因为它允许操作系统将写入的操作推迟到自身高效地执行写入时再进行。 - Billy ONeal
3
@Thantos:你不能以/dev/zero作为基准测试。使用真实文件进行基准测试--像/dev/zero这样的元文件不会匹配IO系统的实际使用,因为它们不读取任何数据。 - Billy ONeal
3
@mcabral:那只会给CPU带来压力,而不是I/O系统。 @lh3: CPU开销对于IO系统并不重要,更加重要的是系统如何有效利用实际使用的I/O设备,比如硬盘。能够同时处理多个读取请求的解决方案会使用更多的CPU时间,但是当你将它们应用于实际的I/O负载(例如硬盘)时,性能会更好。如果@Thanatos在他的基准测试中使用一个真实的文件,我会将我的踩票改为赞成票。 - Billy ONeal
7
看到有多少人在大谈早期优化,仿佛他们对提问者手头的任务更有专业知识一样,这真是令人娱乐。我们提出这些问题是为了寻找实际优化我们代码的方法,而不是听到Knuth哲学的重申。 - rr-
显示剩余9条评论

21

如果你想超越C规范,使用操作系统特定的代码,那么内存映射通常被认为是最有效的方法。

对于Posix,请查看 mmap,对于Windows,请查看 OpenFileMapping


7
实际上,您需要使用CreateFileMapping和MapViewOfFile。OpenFileMapping仅用于打开现有的命名内存映射文件对象,例如共享内存。但是感谢您建议使用内存映射。 - Billy ONeal
1
虽然内存映射在易用性和高效性方面是最简单的,但实际上它并不是最高效的。未缓存且可能异步的I/O将更快、更可扩展,至少在Windows上是如此。 - Cory Nelson
@CoryNelson 如果你只想顺序阅读它,它也不是最快的。 - Kaihaku

9

你遇到了什么问题?

如果你需要最快的文件读取速度(同时还要与操作系统兼容),直接使用操作系统调用,并确保你学会了如何最有效地使用它们。

  1. 你的数据物理布局是怎样的?例如,旋转驱动器可能更快地读取存储在边缘的数据,你想要最小化或消除寻道时间。
  2. 你的数据是否预处理?你需要在从磁盘加载数据和使用数据之间进行一些处理吗?
  3. 最佳块大小是多少?(它可能是扇区大小的某个偶数倍。查看你的操作系统文档。)

如果寻道时间成为问题,请重新安排磁盘上的数据(如果可以),并将其存储在较大的、预处理的文件中,而不是从这里和那里加载小块。

如果数据传输时间成为问题,也许可以考虑压缩数据。


2
对于那位一直在踩我的人:我很想知道为什么。是因为我的回答是错误或者误导性的吗? - Matt Curtis

2

我在考虑read系统调用。

请记住,fread是“read”的包装器。

另一方面,fread具有内部缓冲区,因此“read”可能更快,但我认为“fread”将更有效率。


3
请注意,这是针对POSIX特定的。 - Billy ONeal
它只在某些情况下才有效,即如果您的访问模式与其缓冲行为相匹配。 - Matt Curtis

2
如果 fread 很慢,那是因为它额外增加了一些层到底层操作系统机制的读取文件,并干扰你特定程序使用 fread 的方式。换句话说,它很慢是因为你没有按照它被优化的方式使用。
虽然如此,更快的文件读取可以通过了解操作系统 I/O 函数的工作原理,并提供处理程序特定 I/O 访问模式更好的自己的抽象来完成。大多数情况下,您可以通过内存映射文件来实现这一点。
但是,如果您达到了运行机器的极限,内存映射可能不足够。此时,真正由您来找出如何优化您的 I/O 代码。

2

虽然不是最快的,但它表现相当好且简短。

#include <fcntl.h>
#include <unistd.h>

int main() {
    int f = open("file1", O_RDWR);

    char buffer[4096];

    while ( read(f, buffer, 4096) > 0 ) {
        printf("%s", buffer);
    }

}

直接使用系统调用的问题在于,当进程接收到信号时,它们会以“EINTR”退出,因此您可能需要手动重试。 - wonder.mice

1

也许可以看看 Perl 是如何做的。Perl 的 I/O 例程是经过优化的,我了解到这也是使用 Perl 过滤器处理文本比使用 sed 进行相同转换快两倍的原因。

显然,Perl 相当复杂,而 I/O 只是它所做的一小部分。我从未查看过它的源代码,所以除了指向 这里,我无法给出更好的指导。


0
一些人在这里指出的问题是,在取决于源、目标缓冲区大小等因素的情况下,你可以为特定情况创建自定义处理程序,但在其他情况下(如块/字符设备,即/dev/*),像那样的标准规则可能适用或不适用,而且你的后台源可能是一些以串行方式弹出字符而没有任何缓冲的东西,比如I2C总线,标准RS-232等等。还有一些其他来源,其中字符设备是内存可映射的大段内存,例如Nvidia的视频驱动器字符设备(/dev/nvidiactl)。
许多人在高性能应用程序中选择的另一个设计实现是使用异步而不是同步I/O来处理数据读取方式。研究一下libaio,以及提供预打包解决方案的libaio的移植版本,同时研究一下在工作线程和消费者线程之间使用共享内存的read(但请记住,如果你选择这条路,这将增加编程复杂性)。异步I/O也是一些你不能通过stdio获得的标准OS系统调用。只要小心,因为read中有些位是根据规范“可移植”的,但并不是所有操作系统(如FreeBSD)都支持POSIX STREAMs(出于自己的原因)。

你可以尝试另一种方法(取决于你的数据可移植性),即压缩和/或转换为二进制格式,如数据库格式(例如BDB、SQL等)。某些数据库格式可以使用字节序转换函数在不同机器之间进行移植。

通常最好采用一组算法和方法,使用不同的方法运行性能测试,并评估最适合你的应用程序的平均任务的最佳算法。这将帮助你确定最佳执行算法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接