高频小文件读写应避免 File.ReadAllText,改用 FileStream 配合栈上或复用缓冲区;优先使用 Span/Memory 减少堆分配;异步操作需显式指定 FileOptions.Asynchronous 才真正异步。

用 FileStream 配合缓冲区手动读写,别直接用 File.ReadAllText
高频小文件读写时,File.ReadAllText 和 File.WriteAllText 看似方便,实则每次调用都分配新字符串、触发隐式编码转换、还绕不开内部临时 byte[] 缓冲区——这些全是 GC 的“饲料”。尤其在循环中反复调用,会快速堆积 Gen 0 对象。
实操建议:
- 对已知大小或可预估长度的文件,用
FileStream+ 栈上缓冲区(如stackalloc byte[4096])或复用ArrayPool;.Shared.Rent() - 读文本时,优先用
StreamReader并传入复用的byte[]缓冲区,避免它自己 new; - 写文件时,用
StreamWriter构造函数指定bufferSize(如 8192),并设leaveOpen = true避免重复关闭底层流。
Span 和 Memory 能省掉哪些堆分配
从 .NET Core 2.1 起,文件 I/O API 大量支持 Span,比如 FileStream.Read(Span、Stream.Write(ReadOnlySpan。它们不产生数组引用,也不触发堆分配,直接操作栈内存或已有数组片段。
常见错误现象:把 Span 转成 byte[] 再传给老接口,等于白换;或者在异步方法里捕获局部 Span 到 lambda 中——编译器会直接报错,因为 Span 不能逃逸到堆。
使用场景:
- 解析日志行、CSV 片段、二进制协议头等固定结构数据,全程用
Span切片 +Utf8Parser; - 配合
Encoding.UTF8.GetChars(ReadOnlySpan解码,避免生成中间, Span ) string; - 注意
Memory可以跨 await 边界,但背后若基于ArrayPool,记得.Dispose()或.Return()归还。
异步文件操作不是万能解药,小心 async/await 带来的状态机开销
FileStream.ReadAsync 在大文件或高吞吐场景确实能释放线程,但每次 await 都会生成一个状态机对象(Gen 0),如果每毫秒都读一次小块数据,GC 压力反而比同步+缓冲更大。
性能影响:
- 短时高频小读写(如配置热重载、监控采样),同步 + 大缓冲区 + 复用
FileStream实例更稳; - 真正耗时操作(> 10ms,如 GB 级日志归档),才值得上
ReadAsync; - 不要对单个
FileStream多路并发ReadAsync——Windows 上会退化为同步模拟,Linux 上可能触发epoll误判,不如分拆成多个流或改用MemoryMappedFile。
FileOptions.Asynchronous 必须和 FileStream 构造绑定生效
很多人以为只要调用 ReadAsync 就自动走 IOCP,其实不然。如果创建 FileStream 时没传 FileOptions.Asynchronous,即使后续调用异步方法,底层仍是同步读+线程池线程模拟,白白增加调度开销。
关键点:
- 必须显式传参:
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous); - 缓冲区大小(第 5 个参数)建议设为磁盘扇区对齐值(通常 4096),否则系统可能额外拷贝;
- 这个 flag 在 Linux 上无效(.NET 6+ 用
io_uring自动优化),但 Windows 下漏掉就彻底失去异步优势。
GC 压力大的根因往往不在“用了多少 string”,而在于“谁在替你悄悄 new 数组”。缓存流实例、复用缓冲区、堵死 Span 逃逸路径——这几件事做扎实了,Gen 0 次数通常能砍掉七成以上。










