fread函数的工作原理是什么?

79

fread 的声明如下:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

问题是:两个使用 fread 的调用在读取性能上是否有差异:

char a[1000];
  1. fread(a, 1, 1000, stdin);
  2. fread(a, 1000, 1, stdin);

这两行代码会一次性读取1000字节吗?

7个回答

110

性能上可能有差异,语义上有区别。

fread(a, 1, 1000, stdin);

尝试读取1000个数据元素,每个元素长度为1字节。

fread(a, 1000, 1, stdin);

尝试读取一个长度为1000字节的数据元素。

它们之间的差异在于fread()返回它能够读取的数据元素数量,而不是字节数。如果在读取完整的1000字节之前达到文件结尾(或出现错误),第一种情况必须指示实际读取了多少字节;第二种情况将失败并返回0。

实际上,它可能只会调用低级别的函数来尝试读取1000字节,并指示实际读取了多少字节。对于更大的读取,它可能会进行多个低级别的调用。计算fread()要返回的值是不同的,但计算的开销微不足道。

如果在尝试读取数据之前实现可以确定没有足够的数据可读,则可能存在差异。例如,如果您从一个900字节的文件中读取,第一种情况将读取所有900字节并返回900,而第二种情况可能不会做任何事情。在两种情况下,文件位置指示器都会根据成功读取的字符数(即900)进行移动。

但通常,您应该根据需要从中选择调用它的方式。如果部分读取比不读取任何内容更好,则读取单个数据元素。如果部分读取有用,则以较小的块进行读取。


第二个人可能不会读任何东西。在两种情况下,文件位置指示器都将根据成功读取的字符数进行推进,即900。难道在第二个版本中,文件位置指示器不应该前进,因为没有读取任何内容吗?换句话说,fread(a, 1000, N, stdin);是否总是将fp指示器按1000的倍数推进? - Shahbaz
3
没关系,我找到了。C11标准文档中的7.21.8.1.2和7.21.8.2.2节中提到:如果发生错误,流的文件位置指示器的结果值是不确定的。 - Shahbaz
那么没有办法恢复指示器的位置吗?或者避免读取最后一个混淆指示器位置的块? - David 天宇 Wong
1
如果你需要恢复位置,在调用fread前调用ftell,然后在其后调用fseek。 —— @David天宇Wong - Keith Thompson
2
POSIX规范要求更严格...它要求fread每个对象执行size fgetc,因此在任何情况下都将执行完全相同数量的fgetc(但返回值将不同)。 - Jim Balter
显示剩余5条评论

18

那将是一个实现细节。在glibc中,这两者在性能上是相同的,因为它基本上是实现为(参考http://sourceware.org/git/?p=glibc.git;a=blob;f=libio/iofread.c):

size_t fread (void* buf, size_t size, size_t count, FILE* f)
{
    size_t bytes_requested = size * count;
    size_t bytes_read = read(f->fd, buf, bytes_requested);
    return bytes_read / size;
}

注意,C标准和POSIX标准不能保证每次都会读取完整大小为size的对象。如果无法读取完整对象(例如stdin只有999字节,但您请求了size == 1000),则文件将处于不确定状态(C99 §7.19.8.1/2)。

编辑:请参见其他答案关于POSIX的内容。


你提到了POSIX标准,但它要求使用fgetc来实现fread,这比C的要求更加确定性。 - Jim Balter
1
太棒了的回答!!正是每个来到这里的人所需要的!!我很惊讶它竟然只有那么少的票数。 - Sandeep
fwrite也是一样的吗? - Sandeep
重要提示:在读取大于1个大小的记录时,您可以中断文件。 - ArekBulski
@kennythm在满足调用方想要fread1MB字节的要求之前,read可能会被多次调用,然后才会返回fread - John

17

根据规范,实现方式可能会对这两种情况进行不同的处理。

如果您的文件小于1000个字节,则fread(a, 1, 1000, stdin)(每次读取一个字节并重复1000次)仍将复制所有字节直到EOF。另一方面,fread(a, 1000, 1, stdin)(读取1个1000字节的元素)存储在a中的结果是未指定的,因为没有足够的数据来完成读取“第一个”(也是唯一的)1000字节元素。

当然,有些实现可能仍会将“部分”的元素复制到所需的字节数中。


6
fread在内部调用getc。在Minix中,getc被调用的次数就是size*nmemb,因此getc被调用的次数取决于这两个数的乘积。因此,fread(a, 1, 1000, stdin)fread(a, 1000, 1, stdin)都会运行getc 1000=(1000*1)次。以下是来自Minix的fread的简单实现。
size_t fread(void *ptr, size_t size, size_t nmemb, register FILE *stream){
register char *cp = ptr;
register int c;
size_t ndone = 0;
register size_t s;

if (size)
    while ( ndone < nmemb ) {
    s = size;
    do {
        if ((c = getc(stream)) != EOF)
            *cp++ = c;
        else
            return ndone;
    } while (--s);
    ndone++;
}

return ndone;
}

1
在我的观点中,真诚的答案是: - Sathvik

3
可能没有性能差异,但这些调用并不相同。 fread返回读取的元素数量,因此这些调用将返回不同的值。
如果一个元素无法完整读取,其值是不确定的:
如果出现错误,则流的文件位置指示器的结果值是不确定的。如果读取了部分元素,则其值是不确定的。(ISO / IEC 9899:TC2 7.19.8.1)
在glibc实现中,几乎没有什么区别,它只是通过将元素大小乘以元素数来确定要读取多少字节,并在最后将读取的总量除以成员大小。但指定元素大小为1的版本始终会告诉您读取的正确字节数。但是,如果您只关心完全读取的特定大小的元素,则使用另一种形式可以避免进行除法运算。

1

http://pubs.opengroup.org/onlinepubs/000095399/functions/fread.html中还有一个值得注意的句子

fread()函数将从指向流的指针中读取大小为size字节的nitems个元素到ptr指向的数组中。对于每个对象,将调用size次fgetc()函数并将结果按读取顺序存储在一个无符号字符数组中,正好覆盖该对象。

简而言之,在两种情况下都将通过fgetc()访问数据...!


是的,我也有同感,但页面上写着:"此参考页面所描述的功能与 ISO C 标准一致"。这似乎让人感到不确定? - Jeegar Patel
1
@Mr.32:标准对fgetc的调用也是一样的,所以Posix确实与C99对齐。但标准并没有为符合规范的程序提供任何方法来确定是否“真正”调用了fgetc,或者fread是否执行了其他等效操作。5.1.2.3解释了标准仅描述了“抽象机器”的行为,并列出了实际程序必须匹配该行为的方式。这在C++中被称为“as-if”规则,而不是C(我之前的错误)。非可观察行为无需相同。 - Steve Jessop
因此,即使特定的实现提供了一些方法来计算fgetc被调用的次数(例如通过让您链接程序到自己版本的该函数,例如通过修改和重新编译libc),它也可以这样做,但前提是您替换的函数不总是并且仅在标准描述抽象机器调用它时才被调用。 - Steve Jessop
@SteveJessop“不可观测的行为不一定相同。”那么为什么它在POSIX中有记录? - Roman Byshko
@初学者:因为抽象机器行为的描述是描述fread(或任何其他C代码)效果的便捷方式。这在Posix中是以这种方式记录的,仅仅是因为标准是这样记录的。 - Steve Jessop
@SteveJessop,在使用fgetc函数的库实现和手动实现之间确实存在可以检测到的差异,而且这种差异是不符合标准的。当然,人们可以就“可检测性”达成一致的定义进行讨论。 - Jim Balter

1

我想澄清这里的答案。fread执行缓冲IO。fread使用的实际读取块大小由所使用的C实现确定。

所有现代C库在这两个调用中都具有相同的性能:

fread(a, 1, 1000, file);
fread(a, 1000, 1, file);

即使是像这样的东西:

for (int i=0; i<1000; i++)
  a[i] = fgetc(file)

应该会产生相同的磁盘访问模式,尽管由于更多调用标准C库和在某些情况下需要磁盘执行额外的查找(本来可以被优化掉),fgetc会更慢。

回到两种形式的fread之间的区别。前者返回实际读取的字节数。后者如果文件大小小于1000,则返回0,否则返回1。在两种情况下,缓冲区都将填充相同的数据,即文件内容最多1000个字节。

一般来说,您可能希望保持第二个参数(大小)设置为1,以便获得读取的字节数。


所有现代的C库在这两个调用中都会有相同的性能 - 是的。但在某些情况下,需要磁盘执行额外的寻道操作,否则本来可以被优化掉的 - 不是。fgetc只是从stdio的内存缓冲区中读取。即使流被设置为无缓冲,底层操作系统也会缓冲磁盘读取。 - Jim Balter
@Jim:fgetc 与 fread 以不同的方式从 stdio 中读取数据。显而易见的结果是,fgetc 总是会最大化寻址/系统调用的数量(不好),而 fread 则会最小化寻址/系统调用的数量,因为你向 libc 提供了更多关于你正在做什么的信息。 - Clarus
2
对不起,但你并不知道你在说什么… 没有任何方式可以使fread或fgetc的区别影响到查找次数,并且你没有提供任何支持这个荒谬说法的证据。请注意,在C99和POSIX标准中,fread的定义是以fgetc为基础的,正如本页面其他地方所讨论的那样。 - Jim Balter

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