在C语言中,fgetc / fputc和fread / fwrite的速度比较

9

所以(只是为了好玩),我试图编写一个C代码来复制文件。我查阅了一些资料,似乎所有从流中读取的函数都调用fgetc()(希望这是真的?),因此我使用了该函数:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define FILEr "img1.png"
#define FILEw "img2.png"
main()
{
    clock_t start,diff;
    int msec;
    FILE *fr,*fw;
    fr=fopen(FILEr,"r");
    fw=fopen(FILEw,"w");
    start=clock();
    while((!feof(fr)))
        fputc(fgetc(fr),fw);
    diff=clock()-start;
    msec=diff*1000/CLOCKS_PER_SEC;
    printf("Time taken %d seconds %d milliseconds\n", msec/1000, msec%1000);
    fclose(fr);
    fclose(fw);
}

这个this文件在一台2.10GHz的core2Duo T6500 Dell Inspiron笔记本电脑上运行时间为140毫秒。然而,当我尝试使用fread/fwrite时,随着每次调用传输的字节数(即下面代码中的变量st)不断增加,运行时间逐渐减少,直到峰值约为10毫秒!以下是代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define FILEr "img1.png"
#define FILEw "img2.png"
main()
{
    clock_t start,diff;
    // number of bytes copied at each step
    size_t st=10000;
    int msec;
    FILE *fr,*fw;
    // placeholder for value that is read
    char *x;
    x=malloc(st);
    fr=fopen(FILEr,"r");
    fw=fopen(FILEw,"w");
    start=clock();
    while(!feof(fr))
     {
        fread(x,1,st,fr);
        fwrite(x,1,st,fw);
     }
    diff=clock()-start;
    msec=diff*1000/CLOCKS_PER_SEC;
    printf("Time taken %d seconds %d milliseconds\n", msec/1000, msec%1000);
    fclose(fr);
    fclose(fw);
    free(x);
}

为什么会发生这种情况?即如果fread实际上是多次调用fgetc,那么为什么速度会有差异? 编辑:指定“增加的字节数”是指第二个代码中的变量st

1
你说你的代码是“复制”文件,但是结果文件与原始文件不相等。你的代码使用了错误的复制模式。 - Roland Illig
4
@pratikm说的是Roland Illig提到的一个事实,即feof()函数只在读取失败后才会返回true,因此你的循环会将最后一个字符/块写入输出文件两次。 - caf
3
应当注意的是,clock 不是一种有效的测量时间的方法。它的结果在某种程度上是由实现定义的,在 POSIX 系统上,它返回进程使用的 CPU 时间而不是真实时间,当涉及到输入/输出时,这可能会产生很大的差异。应该使用 clock_gettime(或 gettimeofday)代替。 - R.. GitHub STOP HELPING ICE
4
使用 cmp 命令可以更加确信地验证完整性。 - R.. GitHub STOP HELPING ICE
对于这种情况,我认为 clock 运行良好,因为这不是一个多线程程序。当涉及到使用 pthreads 时,它就毫无用处了。 - Laksith
显示剩余8条评论
4个回答

22

fread() 不是通过调用 fgetc() 来读取每个字节。

它的行为就像重复调用 fgetc(),但它直接访问 fgetc() 读取数据的缓冲区,因此可以直接复制更大量的数据。


6
"+1 for 'as if'." 意思是对“as if”规则表示认同。你可以通过解释“as if”规则如何适用于C语言中的所有内容来改进答案。 - R.. GitHub STOP HELPING ICE
根据文件系统的实现方式,我认为行为可能非常不带缓冲。我曾经看到一些在FUSE文件系统上构建的基于fuse低级接口的fgetc基准测试非常糟糕。 - sehe
3
用户空间的libc stdio库与FUSE或正在访问的底层文件系统/设备完全没有关系。除非禁用缓冲区,否则它将始终在完全缓冲模式下操作。即使禁用了缓冲区,一个非病态的fread实现也永远不会重复调用fgetc,而是会对请求长度中无法从现有用户空间缓冲区获取的部分执行单个read操作。 - R.. GitHub STOP HELPING ICE

9

您忘记了文件缓冲(inode、dentry和页面缓存)。

在运行之前,请清除它们:

echo 3 > /proc/sys/vm/drop_caches

背景:

基准测试是一门艺术。请参考 bonnie++iozonephoronix 进行适当的文件系统基准测试。作为一个特点,bonnie++不允许对已写入卷少于可用系统内存的两倍进行基准测试。

原因?

(答案:缓冲效应!)


1
与OP的问题无关。 - R.. GitHub STOP HELPING ICE
7
可以的。很可能操作员将在短时间内测试两个版本。第一次运行将填充缓存,第二次可以期望文件完全缓冲到操作系统缓冲区中,因此操作将不需要进行I/O。 - wildplasser
@R.. 这在很大程度上取决于您的解释。我认为问题不是非常清楚,但我认为 OP 正在进行越来越大的体积基准测试,但看到运行时间越来越短。这对我来说显然意味着缓存效应。无论如何,由于 FS 基准测试很难,并且 OP 没有采取防止错误结果的步骤,我倾向于假设 OP 不知道它。 - sehe
鉴于silly feof()的使用以及他对性能的关注,很明显我认为OP不知道自己在做什么。而R.因上述评论而获得赞同表明OP并不孤单。 - wildplasser
@wildplasser:公正地说,另一个答案也不是没有价值的。有很多因素在起作用。文件系统基准测试很难 :) 所以我认为两个答案都与OP的问题密切相关。 - sehe
在任何现代系统上,无论使用哪种读取方法,测试的文件都将被内核完全缓存在内存中。OP的问题是关于stdio和freadfgetc的区别,而不是文件系统缓存问题。 - R.. GitHub STOP HELPING ICE

4

就像sehe所说的那样,这部分原因是缓冲,但还有更多,我将解释为什么会这样,并且同时解释为什么fgetc()会导致更多的延迟。

fgetc()在从文件中读取每个字节时都会被调用。

fread()在读取本地缓冲区文件数据的每n个字节时都会被调用。

因此对于一个10MiB的文件:

fgetc()被调用:10,485,760次

而使用1KiB缓冲区的fread()函数只被调用了10,240次。

假设每个函数调用需要1毫秒:

fgetc()需要10,485,760毫秒=10485.76秒〜2.9127小时 fread()需要10,240毫秒=10.24秒

此外,操作系统通常在同一设备上进行读写操作,我猜你的示例是在同一硬盘上执行。当操作系统读取源文件时,移动硬盘头到旋转的磁盘上寻找文件,然后读取1个字节,将其放入内存,然后再次移动读/写头到硬盘旋转盘上查找操作系统和硬盘控制器约定的目标文件位置,并从内存中写入1个字节。对于上面的示例,每个文件会发生1000万次以上:总计超过2000万次,使用缓冲版本只会发生超过20000次。
此外,操作系统在读取磁盘时为了提高性能会将更多KiB的硬盘数据放入内存,即使使用效率较低的fgetc也可以加快程序运行速度,因为程序从操作系统内存中读取数据而不是直接从硬盘读取。这就是sehe回答所涉及的内容。
根据您的机器配置/负载/操作系统等情况,读写结果可能会有很大的差异,因此他建议清空磁盘缓存以获得更准确、更有意义的结果。
当源文件和目标文件在不同的硬盘上时,速度会更快。对于SSD,我不确定读/写是否绝对彼此独立。每次调用函数都有一定的开销,从硬盘读取有其他开销,缓存/缓冲区有助于加快速度。其他信息。

http://en.wikipedia.org/wiki/Disk_read-and-write_head

http://en.wikipedia.org/wiki/Hard_disk#Components


3
stdio函数会填充一个大小为"BUFSIZ"(在stdio.h中定义)的读取缓冲区,并且每当该缓冲区被耗尽时,它只会进行一次read(2)系统调用。它们不会为每个消耗的字节执行单独的read(2)系统调用——它们读取大块内容。BUFSIZ通常是1024或4096之类的值。
您也可以根据需要调整该缓冲区的大小,以增加其大小——在大多数系统上,可以查看setbuf/setvbuf/setbuffer的手册页——但这不太可能对性能产生巨大影响。
另一方面,正如您所指出的,您可以通过在调用中设置该大小来进行任意大小的read(2)系统调用,尽管在某些时候你会得到递减的回报。
顺便说一下,如果您是这样操作的,那么最好使用open(2)而不是fopen(3)。没有必要打开仅用于其文件描述符的文件。

谢谢,这很有道理,我想使用fopen可能有点愚蠢哈哈...不过我对你的回答还有一个后续问题:你说可以通过在调用中设置大小来使read(2)系统调用任意大小,但是你会得到递减的回报。你具体指的是什么?事情会如何出错或变得不那么高效? - Matthew Fitzpatrick
我认为Perry所说的是,在读取的块的大小趋近无限大时,freadread之间的性能差异应该接近于零。在一个好的stdio实现中,这绝对是正确的,但是如果通过许多小型底层的“read”首先将数据通过缓冲区读取并将其复制到调用方的缓冲区中,那么一个不好的实现可能会始终比fread慢... - R.. GitHub STOP HELPING ICE
@R..,我更多地回答了“为什么增加read(2)调用的大小会遇到递减收益”的问题。当然,您的评论也是正确的 - 通常,stdio实现将使用固定大小的缓冲区,因此与缓冲区大小相比,大块的fread不会像可比较的read(2)调用那样高效。 - Perry
嗯,我会认真质疑一个stdio实现的可行性,即使可以直接从调用者的缓冲区读取或直接写入,它也强制所有数据通过缓冲区传输。就像我会质疑重复调用fgetc而不是只是重复调用它的“as if”行为一样 - 但显然确实存在这样的糟糕实现! :-) - R.. GitHub STOP HELPING ICE
在一般情况下,实现很难避免缓冲区,因为满足读取所需的部分数据可能已经在缓冲区中。我最近没有查看过BSD和Linux中fread的实现 - 你可能是正确的,在复制缓冲区后,它们会直接满足下一部分读取到用户的缓冲区中。当然,标准并不要求这样做,但如果不查看实际操作,我就不知道实际情况如何。但我们现在已经远离@MatthewFitzpatrick最初的问题了... - Perry
显示剩余2条评论

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