(为有点冗长的介绍道歉)
在开发一个将整个大文件 (>400MB) 预置到缓冲区缓存中以加速以后实际运行的应用程序的开发过程中,我测试了一次读取 4MB 是否仍然比一次仅读取 1MB 块有任何明显的好处。令人惊讶的是,较小的请求实际上速度更快。这似乎违反直觉,所以我进行了更广泛的测试。
缓冲区缓存在运行测试之前被清除(只是为了笑,我也对缓冲区中的文件进行了一次运行。无论请求大小如何,缓冲区缓存都提供超过 2GB/s 的速度,尽管令人惊讶的是 +/- 30%随机方差)。
所有读取都使用与相同目标缓冲区重叠的 ReadFile(句柄打开时使用FILE_FLAG_OVERLAPPED
和不使用 FILE_FLAG_NO_BUFFERING
)。使用的硬盘有些陈旧但功能齐全,NTFS 的集群大小为 8kB。磁盘在初始运行后进行了碎片整理(6 个碎片与未碎片化,零差异)。为了获得更好的数字,我也使用了更大的文件,以下数字用于读取 1GB。
结果真的很令人惊讶:
4MB x 256 : 5ms per request, completion 25.8s @ ~40 MB/s
1MB x 1024 : 11.7ms per request, completion 23.3s @ ~43 MB/s
32kB x 32768 : 12.6ms per request, completion 15.5s @ ~66 MB/s
16kB x 65536 : 12.8ms per request, completion 13.5s @ ~75 MB/s
因此,这表明提交两个集群长度的一万个请求实际上比提交几百个大的连续读取要好。提交时间(ReadFile 返回之前的时间)确实随着请求数量的增加而大幅增加,但异步完成时间几乎减半。
在异步读取完成时,内核 CPU 时间在每种情况下大约为 5-6%(在四核上,所以应该说是 20-30%),这是一个令人惊讶的 CPU 数量——显然操作系统做了一些非也可以忽略不计的忙碌等待。30% CPU 在 2.6 GHz 下持续 25 秒,这对于“无所事事”来说是相当多的周期。
知道如何解释吗?也许这里有人对 Windows 重叠 IO 的内部工作有更深入的了解?或者,您可以使用 ReadFile 读取一兆字节数据的想法是否存在重大问题?
我可以看到 IO 调度程序如何通过最小化搜索来优化多个请求,尤其是当请求是随机访问时(它们不是!)。给定 NCQ 中的一些请求,我还可以看到硬盘如何执行类似的优化。
然而,我们谈论的是荒谬的小请求数量——尽管如此,它们的性能却比看起来合理的要高出 2 倍。
旁注:明显的赢家是内存映射。我几乎倾向于添加“不出所料”,因为我是内存映射的忠实粉丝,但在这种情况下,它确实让我感到惊讶,因为“请求”更小,操作系统应该更无法预测和安排 IO。起初我没有测试内存映射,因为它甚至可以远程竞争似乎违反直觉。你的直觉就这么多,呵呵。
以不同的偏移量重复映射/取消映射视图几乎需要零时间。使用 16MB 视图并使用简单的 for() 循环对每个页面进行故障排除,每页读取一个字节可在 9.2 秒内完成 @ ~111 MB/s。CPU 使用率始终低于 3%(一个核心)。相同的计算机,相同的磁盘,相同的一切。
尽管实际上只创建了一个页面,但 Windows 似乎一次将 8 个页面加载到缓冲区缓存中。每 8 页故障以相同的速度运行并从磁盘加载相同数量的数据,但显示较低的“物理内存”和“系统缓存”指标,并且只有 1/8 的页面故障。随后的读取证明页面仍然明确地位于缓冲区缓存中(没有延迟,没有磁盘活动)。
(可能与Memory-Mapped File is Faster on Huge Sequential Read 的关系非常非常遥远?)
为了使其更具说明性:
更新:
使用FILE_FLAG_SEQUENTIAL_SCAN
似乎在某种程度上“平衡”了 128k 的读取,将性能提高了 100%。另一方面,它会严重影响 512k 和 256k 的读取(您必须想知道为什么?),并且对其他任何内容都没有实际影响。可以说,较小块大小的 MB/s 图看起来更“均匀”,但运行时没有区别。
我可能也找到了较小块大小表现更好的解释。如您所知,如果操作系统可以立即处理请求,即从缓冲区(以及针对各种特定于版本的技术限制),异步请求可能会同步运行。
在考虑实际异步与“立即”异步读取时,人们注意到超过 256k,Windows 异步运行每个异步请求。块大小越小,“立即”处理的请求就越多,即使它们不能立即可用(即 ReadFile 只是同步运行)。我无法确定一个清晰的模式(例如“前 100 个请求”或“超过 1000 个请求”),但请求大小和同步性之间似乎存在反比关系。在 8k 的块大小下,每个异步请求都是同步服务的。
出于某种原因,缓冲同步传输的速度是异步传输的两倍(不知道为什么),因此请求大小越小,整体传输越快,因为更多传输是同步完成的。
对于内存映射预故障,FILE_FLAG_SEQUENTIAL_SCAN 会导致性能图的形状略有不同(有一个“缺口”向后移动了一点),但所花费的总时间完全相同(再次,这令人惊讶,但我不能帮助它)。
更新 2:
无缓冲 IO 使 1M、4M 和 512k 请求测试用例的性能图更高,更“尖峰”,最大值为 90 GB/s,但也有苛刻的最小值,1GB 的整体运行时间在 +/- 0.5 以内缓冲运行的 s(缓冲区大小较小的请求完成速度明显更快,但是,这是因为有超过 2558 个正在进行的请求,返回 ERROR_WORKING_SET_QUOTA)。在所有未缓冲的情况下,测得的 CPU 使用率为零,这不足为奇,因为发生的任何 IO 都通过 DMA 运行。
另一个非常有趣的观察结果FILE_FLAG_NO_BUFFERING
是它显着改变了 API 行为。CancelIO
不再起作用,至少在取消 IO的意义上没有。对于未缓冲的正在进行的请求,CancelIO
将简单地阻塞,直到所有请求都完成。律师可能会争辩说,该功能不能因疏忽其职责而承担责任,因为当它返回时已经没有更多的飞行请求了,所以在某种程度上它已经完成了所要求的——但我对“取消”的理解有点不同。
使用缓冲的重叠 IO,CancelIO
将简单地切断绳索,所有进行中的操作都会立即终止,正如人们所期望的那样。
另一个有趣的事情是,在所有请求都完成或失败之前,该进程是无法终止的。如果操作系统正在对该地址空间进行 DMA,这种做法是有道理的,但它仍然是一个令人惊叹的“功能”。