windows api 层面通过 createfile 传入 file_flag_sequential_scan(0x08000000)可启用顺序读预取,需配合大缓冲区(如64kb)的 readasync 使用,仅对>1mb顺序读有效;filestream 默认不开启,file.readallbytes 虽快但不可控且内存风险高,.net 无全局预取配置。

Windows API 层面的 CreateFile 预取控制
Windows 本身支持文件打开时启用预读(FILE_FLAG_SEQUENTIAL_SCAN),C# 的 FileStream 默认不开启,但可通过底层句柄传递实现。关键不是“让 .NET 自动预取”,而是告诉系统:“我大概率会顺序读完这个文件”。
常见错误是直接用 new FileStream(path, FileMode.Open) 就指望系统聪明——它不会主动推测访问模式,尤其对小文件或随机访问场景,反而可能因预取产生无效 I/O。
- 需显式调用
NativeMethods.CreateFile,传入FILE_FLAG_SEQUENTIAL_SCAN(值为0x08000000) - 再用该句柄构造
FileStream:`new FileStream(handle, FileAccess.Read, bufferSize, isAsync: true)` - 仅对 >1MB 的顺序读场景有效;若后续有大量
Seek,系统会自动退化预取行为 - .NET 6+ 中
FileStreamOptions的PreallocationSize是写优化,和读预取无关
FileStream.ReadAsync 的缓冲区大小与预取效果
预取是否生效,和你每次读多少字节强相关。系统预取器(如 Windows SuperFetch)倾向于按 64KB~256KB 块预加载,但如果你的 ReadAsync 缓冲区只有 4KB,它可能只触发最小粒度预读,甚至被忽略。
典型误用:在循环里反复调用 stream.ReadAsync(buffer, 0, 4096) —— 这本质是“手动模拟随机读”,系统无法识别连续性。
- 建议单次
ReadAsync缓冲区设为65536(64KB)或131072(128KB) - 配合
FileStreamOptions.BufferSize设置相同值,避免内部二次拷贝 - 若文件已映射到内存(如通过
MemoryMappedFile),预取逻辑由 MMF 管理,FileStream的预取标志失效
为什么 File.ReadAllBytes 看起来“很快”但不可控
它快不是因为用了高级预取,而是绕过了流式控制:直接调用 ReadFile 并传入 FILE_FLAG_NO_BUFFERING + 大缓冲区,让内核一次性把能读的都拉进用户空间。但代价是内存占用突增、无法流式处理、且对 >2GB 文件会抛 OutOfMemoryException。
真实业务中容易踩的坑是把它当“性能银弹”用在大日志解析或上传前校验上——结果进程 RSS 暴涨,GC 压力陡升,还掩盖了真正需要流式处理的场景。
-
File.ReadAllBytes不受FILE_FLAG_SEQUENTIAL_SCAN影响,它是同步阻塞调用 - 等价逻辑可手写为:
var buf = new byte[fileLength]; stream.Read(buf, 0, buf.Length),但要注意fileLength必须可信(否则缓冲区溢出) - 若只需校验哈希,用
HashAlgorithm.ComputeHash(stream)流式计算,比全读更稳
.NET 运行时自身不管理磁盘预取
别在 dotnet run 参数或 runtimeconfig.json 里找“预取开关”——.NET 没有这种配置项。所有预取行为最终都下沉到 Windows I/O Manager 或 Linux page cache,C# 层只能通过正确使用 Win32 API 或合理组织读模式来“引导”系统行为。
最容易被忽略的是文件打开方式与生命周期的匹配。比如用 using var fs = File.OpenRead(path) 打开一个 500MB 文件,但只读前 10KB 就 Dispose,系统预取的后续数据块会很快被 page cache 回收,白忙一场。
- 预取收益的前提是:打开后持续、稳定地读取足够多数据(一般 ≥ 预取窗口的 2 倍)
- 频繁短命
FileStream实例( - SSD 上预取收益远低于 HDD,但
FILE_FLAG_SEQUENTIAL_SCAN仍有助于减少 TRIM 和 GC 干扰










