FILE*和文件描述符的读写性能

3
使用FILE*文件描述符API来处理本地磁盘文件的二进制数据读写会产生什么性能影响?这两种方法是否有任何优势?
在性能方面,fread()read()哪个更好?它们在行为、缓存或系统资源使用方面有什么不同?
在性能方面,fwrite()write()哪个更好?它们在行为、缓存或系统资源使用方面有什么不同?

你尝试过比较它们吗?你的结果是什么? - nvoigt
这听起来像是一个测试。 - unwind
@nvoigt 我还没有,因为有很多要考虑的事情,比如缓冲区大小、磁盘文件系统等等。我想得到一个关于这两个API“通用”区别的详细答案。 - Dariusz
3个回答

4

readwrite是系统调用,因此它们在用户空间中是无缓冲的。你提交的所有内容都将直接进入内核。

底层文件系统可能有内部缓冲,但这里最大的性能影响将来自于每次调用时切换到内核空间。

freadfwrite是用户空间库调用,默认情况下是带缓冲的。因此,它们会将你的访问组合在一起,以使其更快(理论上)。

试试自己操作:逐字节从文件中read,然后逐字节从中fread。后者应该快4000倍。

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

int main() {
    struct rusage usage_start, usage_end;

    getrusage(RUSAGE_SELF, &usage_start);

    int fd = open("/dev/zero", O_RDONLY);

    int i = 0x400 * 0x400; // 1 MB

    char c;

    while (i--)
        read(fd, &c, 1);

    close(fd);

    getrusage(RUSAGE_SELF, &usage_end);

    printf("Time used by reading 1MiB: %zu user, %zu system.\n", ((usage_end.ru_utime.tv_sec - usage_start.ru_utime.tv_sec)* 1000000) + usage_end.ru_utime.tv_usec - usage_start.ru_utime.tv_usec, ((usage_end.ru_stime.tv_sec - usage_start.ru_stime.tv_sec)* 1000000) + usage_end.ru_stime.tv_usec - usage_start.ru_stime.tv_usec);

    getrusage(RUSAGE_SELF, &usage_start);

    FILE * fp = fopen("/dev/zero", "r");

    i = 0x400 * 0x400; // 1 MB

    while (i--)
        fread(&c, 1, 1, fp);

    fclose(fp);

    getrusage(RUSAGE_SELF, &usage_end);

    printf("Time used by freading 1MiB: %zu user, %zu system.\n", ((usage_end.ru_utime.tv_sec - usage_start.ru_utime.tv_sec)* 1000000) + usage_end.ru_utime.tv_usec - usage_start.ru_utime.tv_usec, ((usage_end.ru_stime.tv_sec - usage_start.ru_stime.tv_sec)* 1000000) + usage_end.ru_stime.tv_usec - usage_start.ru_stime.tv_usec);

    return 0;
}

我的 OS X 返回值:

Time used by reading 1MiB: 103855 user, 442698 system.
Time used by freading 1MiB: 20146 user, 256 system.
< p> stdio 函数只是在适当的系统调用周围包装优化代码。

这是程序的strace

getrusage(RUSAGE_SELF, {ru_utime={0, 0}, ru_stime={0, 0}, ...}) = 0
open("/dev/zero", O_RDONLY)             = 3

接下来是1048576次。
read(3, "\0", 1)                        = 1

以及其余的内容:

close(3)                                = 0
getrusage(RUSAGE_SELF, {ru_utime={0, 200000}, ru_stime={5, 460000}, ...}) = 0

这是fopen的一部分:
fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2aaaaaaae000

getrusage(RUSAGE_SELF, {ru_utime={0, 200000}, ru_stime={5, 460000}, ...}) = 0
// ...
open("/dev/zero", O_RDONLY)             = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 5), ...}) = 0
ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fffffffb050) = -1 ENOTTY (Inappropriate ioctl for device)
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2aaaaaaaf000

现在256倍:
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096

请注意,尽管我是逐字逐句阅读的,但stdio库却一次获取一个页面的文件内容。
其余部分主要是释放内存:
close(3)                                = 0
munmap(0x2aaaaaaaf000, 4096)            = 0
getrusage(RUSAGE_SELF, {ru_utime={0, 230000}, ru_stime={5, 460000}, ...}) = 0
write(1, "Time used by reading 1MiB: 20000"..., 106Time used by reading 1MiB: 200000 user, 5460000 system.
Time used by freading 1MiB: 30000 user, 0 system.
) = 106
exit_group(0)                           = ?

谢谢你的努力。然而,这个测试有些片面,它只显示fread确实是有缓冲的。尽管我承认这是一个重要因素,但这不是唯一的因素。 - Dariusz
@Dariusz 除了用户空间缓冲和stdio库的一些额外便利函数(f*调用)之外,功能上没有太大的区别。通常情况下,建议使用stdio而不是系统调用,除非您需要对文件描述符进行非常具体的控制(主要是与操作系统相关的内容),否则这将是不可能的。如果存在底层文件系统缓冲,则无论哪种方式都无法解决此问题,但stdio通常更加优化。 - Sergey L.
你的回答是迄今为止最详细的,所以我接受了它。谢谢! - Dariusz

2
关于访问磁盘上的文件,答案是:这取决于情况。较高级别的函数可以启用缓冲,从而减少物理I/O的数量,这意味着它可以减少实际进行的read()/write()调用的数量(例如fread()调用read()来访问磁盘等)。因此,启用缓冲的高级函数具有优势,通常会看到更好的性能,而无需考虑自己在做什么。低级函数具有优势,如果您知道应用程序将如何执行操作,则可以通过直接管理自己的缓冲区来提高性能。

1

fread/fwrite比read/write快,我同意,但是:

1)如果文件将被随机访问,则fwrite/fread可能无法有效使用,并且大部分时间它们可能会导致性能损失。

2)如果文件正在被另一个进程或线程共享,则其速度不会很快,并且除非每次写入文件时都使用flush()命令,否则不能使用它们。在这种情况下,速度至少会降低到与write命令相同的水平。此外,fread命令不能使用,因为它使用其缓冲区读取数据,这些数据可能没有更新,如果它关心更新,则必须放弃已读取的内容以读取新数据。

所以,这取决于具体情况。


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