为什么读取数据块比在文件I / O中逐字节读取更快

我注意到,逐个读取文件比使用fread读取文件需要更多时间来读取整个文件。

根据cplusplus :
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

从流中读取一个count元素数组,每个元素的大小都是字节size ,并将它们存储在ptr指定的内存块中。

Q1)因此,再次fread读取文件1个字节,所以它不是以1字节方法读取的方式吗?

Q2)结果certificate,仍然可以花费更少的时间。

从这里 :

我使用大约44兆字节的文件作为输入运行它。 使用VC ++ 2012编译时,我得到以下结果:

使用getc计数:400000时间:2.034
使用fread数:400000时间:0.257

SO上的post也很少谈论它取决于操作系统。
Q3)操作系统的作用是什么?

为什么会如此以及幕后背后到底是什么?

fread不读取一个字节的文件。 该界面允许您单独指定sizecount ,纯粹是为了您的方便。 在幕后, fread将只读取size * count个字节。

fread将尝试一次读取的字节数高度依赖于您的C实现和底层文件系统。 除非你对两者都非常熟悉,否则通常可以安全地假设fread将比你自己发明的任何东西更接近最优。

编辑:物理磁盘与其吞吐量相比往往具有相对较高的寻道时间。 换句话说,他们需要相对较长的时间才能开始阅读。 但是一旦启动,它们可以相对快速地读取连续的字节。 因此,如果没有任何OS /文件系统支持,任何对fread调用都会导致开始每次读取的严重开销。 因此,要有效地利用磁盘,您需要尽可能多地读取多个字节。 但是与CPU,RAM和物理缓存相比,磁盘速度很慢。 一次读取太多意味着你的程序花了很多时间等待磁盘完成读取,当它本来可以做一些有用的事情(比如处理已经读取的字节)。

这就是操作系统/文件系统的用武之地。从事这些工作的智能人员花费了大量时间来确定从磁盘请求的正确字节数。 因此,当您调用fread并请求X字节时,OS /文件系统会将其转换为每个Y字节的N请求。 其中Y是一些通常最佳的值,它取决于比这里可以提到的更多的变量。

OS /文件系统的另一个角色是所谓的’readahead’。 基本思想是大多数IO发生在循环内部。 因此,如果一个程序从磁盘请求一些字节,那么它很有可能在不久之后请求下一个字节。 因此,操作系统/文件系统通常会比您实际请求的读取次数略多。 同样,确切的数量取决于太多的变量。 但基本上,这就是为什么一次读取一个字节仍然有些效率(如果没有预读的话,它将是另一个慢10倍)。

最后,最好将fread视为向操作系统/文件系统提供一些关于您想要读取多少字节的提示。 这些提示越准确(越接近您想要读取的总字节数),OS /文件系统将优化磁盘IO越好。

这取决于你如何逐字节读取。 但每次调用fread都会产生很大的开销(可能需要进行OS /内核调用)。

如果您将fread调用1000次以逐个读取1000个字节,那么您需要支付1000倍的费用; 如果您调用fread一次读取1000个字节,那么您只需支付一次该费用。

考虑一下磁盘的实际情况。 每当你要求它进行读取时,它的头部必须寻找正确的位置,然后等待盘片的右边部分旋转。 如果你进行100次单独的1字节读取,你必须这样做100次(作为第一次近似;实际上,操作系统可能有一个足够聪明的缓存策略,可以弄清楚你要做的事情并提前阅读)。 但是如果你在一个操作中读取100个字节,并且那些字节在磁盘上大致是连续的,那么你只需要完成所有这一次。

Hans Passant关于缓存的评论也是正确的,但即使没有这种影响,我也希望1次批量读取操作比许多小型操作更快。

Protip:使用你的探查器来识别实际的真实问题中最重要的瓶颈……

Q1)因此,再次fread读取文件1个字节,所以它不是以1字节方法读取的方式吗?

手册中是否有任何内容表明只能一次读取一个字节? 闪存越来越普遍,通常要求您的操作系统一次读取大小为512KB的块。 也许您的操作系统会为您的利益执行缓冲,因此您无需检查全部金额…

Q2)结果certificate,仍然可以花费更少的时间。

从逻辑上讲,这是一个谬论。 没有要求fgetc在检索字节块时比fread更慢。 事实上,优化编译器可能会在优化解析后生成相同的机器代码。

实际上,它也certificate是无效的。 大多数certificate(例如,你引用的那些证据)忽略了考虑setvbuf (或C ++中的stream.rdbuf()->pubsetbuf )的影响。

然而,下面的经validation据集成了setvbuf ,并且至少在我测试过的每个实现上,已经表明fgetc大致与读取大块数据时的fread一样快,在一些无意义的误差范围内摆动方式…请多次运行这些测试,如果您发现其中一个系统明显快于另一个系统,请告诉我。 我怀疑你不会。 从这段代码中可以构建两个程序:

 gcc -o fread_version -std=c99 file.c gcc -o fgetc_version -std=c99 -DUSE_FGETC file.c 

编译test_file两个程序后,生成一个包含大量字节的test_file ,您可以这样测试:

 time cat test_file | fread_version time cat test_file | fgetc_version 

没有进一步的说明,这里是代码:

 #include  #include  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; } 

PS您可能甚至注意到fgetc版本比fread版本更简单; 它不需要嵌套循环来遍历字符。 这应该是要带走的教训,在这里:编写代码并考虑维护而不是性能。 如有必要,您通常可以提供提示(例如setvbuf )以优化您使用探查器识别的瓶颈。

PPS你确实使用你的探查器将其识别为实际现实问题的瓶颈,对吗?

减速的其他贡献者是指令管道重载和数据总线争用。 数据缓存未命中类似于指令管道重新加载,因此我不在此处提供它们。

函数调用和指令管道

在内部,处理器在高速缓存中具有指令流水线(物理上靠近处理器的快速存储器)。 处理器将用指令填充管道,然后执行指令并再次填满管道。 (注意,某些处理器可能会在管道中打开的插槽中获取指令)。

执行函数调用时,处理器遇到分支语句。 在分支解析之前,处理器无法将任何新指令提取到管道中。 如果执行分支,则管道可能正在重新加载,浪费时间。 (注意:某些处理器可以在缓存中读入足够的指令,因此不需要读取指令。例如,一个小循环。)

最坏的情况是,当您调用读取函数1000次时,您将导致1000次重新加载指令管道。 如果调用read函数一次,则只重新加载一次管道。

Databus Collisions
数据通过数据总线从硬盘驱动器流向处理器,然后从处理器流向存储器。 某些平台允许从硬盘驱动器到内存的直接内存访问(DMA)。 在任何一种情况下,都存在多个用户与数据总线的争用。

最有效地使用数据总线是发送大块数据。 当用户(组件,例如处理器或DMA)想要使用数据总线时,用户必须等待它变得可用。 最坏的情况是,另一个用户正在发送大块,因此有很长的延迟。 当一次发送1000个字节时,用户必须等待1000次以便其他用户放弃数据总线的时间。

在市场或餐馆的队列(线)等待的图片。 您需要购买许多物品,但是您购买了一件物品,然后必须再次排队等候。 或者您可以像其他购物者一样购买许多商品。 哪个消耗更多时间?

摘要
使用大块进行I / O传输的原因有很多。 一些原因是物理驱动器,其他原因涉及指令管道,数据缓存和数据总线争用。 通过减少数据请求的数量并增加数据大小,累积时间也减少了。 一个请求的开销少于1000个请求。 如果开销是1毫秒,则一个请求需要1毫秒,而1000个请求需要1秒。