我注意到,逐字节读取文件比使用fread
读取文件需要更多时间来读取整个文件。
根据cplusplus:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从流中读取count
个元素的数组,每个元素的大小为size
个字节,并将它们存储在ptr
指定的内存块中。
Q1)因此,再次fread
将文件读取1个字节,所以它是否与通过1字节方法读取的方式相同?
Q2)结果证明仍然fread
花费更少的时间。
来自here:
我用大约44兆字节的文件作为输入运行它。使用VC ++ 2012编译时,我得到了以下结果:
使用getc计数:400000时间:2.034
使用fread数:400000时间:0.257
SO上的帖子也很少谈论它取决于操作系统 Q3)操作系统的作用是什么?
为什么会如此以及幕后背后到底是什么?
答案 0 :(得分:15)
fread
不一次一个字节地读取文件。该界面允许您单独指定size
和count
,纯粹是为了您的方便。在幕后,fread
只会读取size * count
字节。
fread
尝试在处读取的字节数高度依赖于C实现和底层文件系统。除非你对两者都非常熟悉,否则通常可以安全地假设fread
将比你自己发明的任何东西更接近最优。
编辑:物理磁盘与其吞吐量相比往往具有相对较高的寻道时间。换句话说,他们开始阅读需要相对较长的时间。但是一旦启动,它们可以相对快速地读取连续的字节。因此,如果没有任何OS /文件系统支持,对fread
的任何调用都将导致开始每次读取的严重开销。因此,为了有效地利用磁盘,您需要尽可能多地读取多个字节。但是与CPU,RAM和物理缓存相比,磁盘速度很慢。一次读取太多意味着你的程序花了很多时间等待磁盘完成读取,当它本来可以做一些有用的事情时(比如处理已读取的字节)。
这就是操作系统/文件系统的用武之地。从事这些工作的智能人员花费了大量时间来确定从磁盘请求的正确字节数。因此,当您调用fread
并请求X
个字节时,操作系统/文件系统会将其转换为每个N
字节的Y
个请求。其中Y
是一些通常最佳的值,它取决于比这里可以提到的更多的变量。
OS /文件系统的另一个角色是所谓的'readahead'。基本思想是大多数IO发生在循环内部。因此,如果一个程序从磁盘请求一些字节,那么它很有可能在不久之后请求下一个字节。因此,操作系统/文件系统通常会比您实际请求的读取次数略多。同样,确切的数量取决于要提及的太多变量。但基本上,这就是为什么一次读取一个字节仍然有些效率(如果没有预读的话,它会再慢10倍)。
最后,最好将fread
视为向操作系统/文件系统提供一些关于您想要读取多少字节的提示。这些提示越准确(越接近您想要读取的总字节数),OS /文件系统将优化磁盘IO越好。
答案 1 :(得分:2)
这取决于您如何逐字节读取。但每次调用fread
都会产生很大的开销(它可能需要进行OS /内核调用)。
如果您拨打fread
1000次,逐个读取1000个字节,那么您需要支付1000倍的费用;如果您拨打fread
一次读取1000个字节,那么您只需支付一次该费用。
答案 2 :(得分:2)
考虑一下磁盘的实际情况。每当你要求它进行读取时,它的头部必须寻找正确的位置,然后等待盘片的右边部分旋转。如果你进行100次单独的1字节读取,你必须这样做100次(作为第一次近似;实际上,操作系统可能有一个足够聪明的缓存策略,可以弄清楚你正在尝试做什么并提前阅读)。但是如果你在一个操作中读取100个字节,并且这些字节在磁盘上大致是连续的,那么你只需要完成所有这一次。
Hans Passant关于缓存的评论也是正确的,但即使没有这种效果,我也希望1次批量读取操作比许多小型操作更快。
答案 3 :(得分:2)
Protip:使用您的探查器识别实际真实问题中最重要的瓶颈......
Q1)因此,再次
fread
将文件读取1个字节,所以它是否与通过1字节方法读取的方式相同?
手册中是否有任何内容表明只能一次读取一个字节?闪存越来越普遍,通常要求您的操作系统一次读取大小为512KB的块。也许您的操作系统会为您的利益执行缓冲,因此您无需检查整个数量......
Q2)结果证明,
fread
时间仍然较短。
从逻辑上讲,这是一个谬论。没有要求fgetc
检索字节块比fread
慢。实际上,优化解析器可以在优化解析后生成相同的机器代码。
实际上,它也证明是无效的。大多数证据(例如,您引用的证据)忽略了setvbuf
(或stream.rdbuf()->pubsetbuf
在C ++中)的影响。
然而,下面的经验证据整合了setvbuf
,并且至少在我测试过的每个实现中,fgetc
在阅读时显示fread
大致与gcc -o fread_version -std=c99 file.c
gcc -o fgetc_version -std=c99 -DUSE_FGETC file.c
一样快一个大块的数据,在一些毫无意义的错误边缘内摆动任何方式......请多次运行这些测试,如果你找到一个系统,其中一个明显比另一个快得多,请告诉我。我怀疑你不会。从这段代码中可以构建两个程序:
test_file
编译完两个程序后,生成一个包含大量字节的time cat test_file | fread_version
time cat test_file | fgetc_version
,您可以这样测试:
#include <assert.h>
#include <stdio.h>
int main(void) {
unsigned int criteria[2] = { 0 };
# ifdef USE_FGETC
int n = setvbuf(stdin, NULL, _IOFBF, 65536);
assert(n == 0);
for (;;) {
int c = fgetc(stdin);
if (c < 0) {
break;
}
criteria[c == 'a']++;
}
# else
char buffer[65536];
for (;;) {
size_t size = fread(buffer, 1, sizeof buffer, stdin);
if (size == 0) {
break;
}
for (size_t x = 0; x < size; x++) {
criteria[buffer[x] == 'a']++;
}
}
# endif
printf("%u %u\n", criteria[0], criteria[1]);
return 0;
}
没有进一步的说明,这里是代码:
fgetc
P.S。您甚至可能已经注意到fread
版本比setvbuf
版本更简单;它不需要嵌套循环来遍历字符。这应该是要带走的教训,在这里:编写代码时考虑到维护,而不是性能。如有必要,您通常可以提供提示(例如int?
)以优化您使用探查器识别的瓶颈。
P.P.S。你确实使用你的探查器将其识别为实际的现实生活中的瓶颈,对吧?
答案 4 :(得分:1)
速度降低的其他贡献者是指令管道重载和数据总线争用。数据缓存未命中类似于指令管道重新加载,因此我不在此处提供它们。
函数调用和指令管道
在内部,处理器在高速缓存中具有指令管道(物理上靠近处理器的快速存储器)。处理器将用指令填充管道,然后执行指令并再次填满管道。 (注意,某些处理器可能会在管道中打开的插槽中获取指令)。
执行函数调用时,处理器遇到分支语句。在分支解析之前,处理器无法将任何新指令提取到管道中。如果执行分支,则管道可能正在重新加载,浪费时间。 (注意:某些处理器可以在缓存中读入足够的指令,因此不需要读取指令。例如,一个小循环。)
最坏的情况是,当您调用读取函数1000次时,会导致1000次重新加载指令管道。如果调用read函数一次,则只重新加载一次管道。
Databus Collisions
数据通过数据总线从硬盘驱动器流向处理器,然后从处理器流向内存。某些平台允许从硬盘驱动器到内存的直接内存访问(DMA)。在任何一种情况下,都存在多个用户与数据总线的争用。
最有效地使用数据总线是发送大块数据。当用户(组件,例如处理器或DMA)想要使用数据总线时,用户必须等待它变得可用。最坏的情况是,另一个用户正在发送大块,因此有很长的延迟。当一次发送1000个字节时,用户必须等待1000次以便其他用户放弃数据总线的时间。
图片在市场或餐厅的队列(线)中等待。您需要购买许多物品,但是您购买了一件物品,然后必须再次排队等候。或者你可以像其他购物者一样购买许多物品。哪个消耗更多时间?
<强>摘要强>
使用大块进行I / O传输的原因有很多。一些原因是物理驱动器,其他原因涉及指令管道,数据缓存和数据总线争用。通过减少数据请求的数量并增加数据大小,累积时间也减少了。一个请求的开销少于1000个请求。如果开销是1毫秒,则一个请求需要1毫秒,而1000个请求需要1秒。