【问题标题】:speed comparison between fgetc/fputc and fread/fwrite in CC中fgetc/fputc和fread/fwrite之间的速度比较
【发布时间】:2011-11-24 00:20:47
【问题描述】:

所以(只是为了好玩),我只是想写一个 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

【问题讨论】:

  • 您说您的代码复制一个文件,但生成的文件将不等于您的原始文件。您的代码使用了错误的复制模式。
  • 您能详细说明一下吗?我复制了一张图片并能够打开副本....与 hdf5 文件相同
  • @pratikm:@Roland Illig 指的是feof() 仅在读取失败后返回true,因此您的循环写入最后一个字符/块输出文件两次。
  • 如果还应注意clock 不是一种有效的时间测量方法。它的结果在某种程度上是实现定义的,在 POSIX 系统上,它返回进程使用的 cpu time 量,而不是 real time 的量 - 这可以当涉及 IO 时会产生很大的不同。请改用clock_gettime(或gettimeofday)。
  • cmp 将是确定性验证完整性的更好方法。

标签: c performance


【解决方案1】:

fread() 没有调用fgetc() 来读取每个字节。

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

【讨论】:

  • +1 表示“好像”。您可以通过解释“好像规则”如何应用于 C 语言中的所有内容来改进答案。
  • @R..:根据存在的文件系统实现,我认为该行为很可能没有缓冲。我已经看到了一些非常可悲的 fgetc 基准测试,其中 FUSE 文件系统构建在 fuse 低级接口之上。
  • 用户空间 libc stdio 库与 FUSE 或被访问的底层文件系统/设备没有任何关系。除非缓冲被禁用,否则它将始终在完全缓冲模式下运行。即使禁用了缓冲,非病态错误的fread 实现也永远不会重复调用fgetc,而是对无法从现有用户空间缓冲区获得的请求长度部分执行单个read 操作.
【解决方案2】:

您忘记了文件缓冲 (inode, dentry and page caches)。

在运行之前清除它们:

echo 3 > /proc/sys/vm/drop_caches

背景:

基准测试是一门艺术。有关正确的文件系统基准测试,请参阅 bonnie++iozonephoronix。作为一个特点,bonnie++ 不允许写入容量小于可用系统内存 2 倍的基准测试。

为什么?

(答案:缓冲效果!)

【讨论】:

  • 与OP的问题无关。
  • 是的。该操作很可能会在短时间内测试这两个版本。第一次运行将启动缓存,第二次运行可以期望文件完全缓冲在 OS 缓冲区中,因此操作不需要 I/O。
  • @R.. 这在很大程度上取决于如何解释它。我认为这个问题并不太清楚,但我认为 OP 正在做越来越大容量基准测试,但看到越来越更短的运行时间。这清楚地向我说明了缓存效果。无论如何,由于 FS 基准测试是 hard 并且 OP 没有采取措施防止错误结果,我倾向于 假设 OP 不知道它。
  • 好吧,考虑到愚蠢的 feof() 用法,以及他对性能的关注,我很清楚 OP 现在并没有他正在做的事情,而且 R. 被投票支持上面的评论表明 OP 并不孤单。
  • @wildplasser:公平地说,其他答案并非没有优点。有很多事情在起作用。 文件系统基准测试很难 :) 所以这两个答案都与 OP 的问题 IMO 非常相关
【解决方案3】:

stdio 函数将填充一个读取缓冲区,其大小为 stdio.h 中定义的“BUFSIZ”,并且每次该缓冲区耗尽时只会进行一次 read(2) 系统调用。他们不会为每个消耗的字节执行单独的 read(2) 系统调用——他们读取大块。 BUFSIZ 通常类似于 1024 或 4096。

如果您愿意,您还可以调整该缓冲区的大小以增加它 - 请参阅大多数系统上的 setbuf/setvbuf/setbuffer 手册页 - 尽管这不太可能对性能产生巨大影响。

另一方面,正如您所注意到的,您可以通过在调用中设置该大小来进行任意大小的 read(2) 系统调用,尽管在某些时候您会得到递减的回报。

顺便说一句,如果你以这种方式做事,你不妨使用 open(2) 而不是 fopen(3)。 fopen'ing 只用于文件描述符的文件没有什么意义。

【讨论】:

  • 谢谢,这很有意义,我想我使用 fopen 有点愚蠢哈哈......不过,我确实对你的回答有一个后续问题:你说你可以通过在调用中设置该大小来读取任意大小的系统调用,但您会得到递减的收益。你到底是什么意思?事情会如何出错或效率降低?
  • @MatthewFitzpatrick:IO 和内存复制的成本总是比系统调用贵得多,以至于读取的字节数没有区别。
  • 减少系统调用的数量可以通过减少系统上下文切换的数量来提高性能,但是一旦你将其减少了一千或四千倍,性能提升的数量就会减少向下。此时您的程序可能不受读取系统调用的支配。事情不会出错,只是性能不会在某个点之后显着提高——实际 I/O 将开始主导 read(2) 所花费的时间,而不是系统调用开销,一旦你超过了一定大小。
  • 我认为佩里的意思是freadread 之间的性能差异应该接近零,因为正在读取的块的大小接近无穷大。在一个好的 stdio 实现上,这绝对是正确的,但是如果它首先通过许多小型底层 reads 中的缓冲区读取数据并将它们复制到调用者的缓冲区,那么在 fread 中可能总是更慢...
  • @R..,我更多的是回答“为什么增加 read(2) 调用的大小会遇到收益递减”。当然,您的评论也是正确的——通常,stdio 实现将使用固定大小的缓冲区,因此对比缓冲区大小大得多的大块的 fread 不会像可比较的 read(2) 调用那样有效。
【解决方案4】:

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

fgetc() 为从文件中读取的每个字节调用。

fread() 为文件数据的本地缓冲区的每 n 个字节调用一次。

所以对于一个 10MiB 的文件:

fgetc() 被调用:10 485 760 次

fread 使用 1KiB 缓冲区时,该函数调用了 10 240 次。

为了简单起见,每个函数调用都需要 1 毫秒: fgetc 需要 10 485 760 毫秒 = 10485.76 秒 ~ 2,9127 小时 fread 需要 10 240 毫秒 = 10.24 秒

除此之外,操作系统通常在同一设备上进行读写,我想您的示例在同一硬盘上进行。操作系统在读取源文件时,将硬盘磁头移动到寻找文件的旋转磁盘盘片上,然后读取 1 个字节,将其放在内存中,然后再次将读/写磁头移动到硬盘旋转盘片上寻找该位置操作系统和硬盘控制器同意定位目标文件,然后从内存中写入 1 个字节。对于上面的示例,每个文件都会发生超过 1000 万次:总计超过 2000 万次,使用缓冲版本,总共会发生超过 20000 次。

除了操作系统在读取磁盘时出于性能目的将更多 KiB 的硬盘数据放入内存之外,即使使用效率较低的fgetc,这也可以加快程序速度,因为程序从操作系统内存中读取而不是直接从硬盘读取。这就是sehe的回应所指的。

根据您的机器配置/负载/操作系统/等,您的读写结果可能会有很大差异,因此他建议清空磁盘缓存以获取更好更有意义的结果。

当源文件和目标文件位于不同的硬盘上时,速度会快很多。对于 SDD,我不确定读/写是否绝对相互排斥。

总结:对函数的每次调用都有一定的开销,从 HDD 读取有其他开销,而缓存/缓冲区有助于加快处理速度。

其他信息

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

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

【讨论】:

    猜你喜欢
    • 2011-09-27
    • 1970-01-01
    • 1970-01-01
    • 2016-12-29
    • 1970-01-01
    • 2015-08-12
    • 2020-12-14
    • 2016-05-23
    相关资源
    最近更新 更多