应使用 streamreader 按行流式读取 gb 级文件,避免 file.readalllines() 导致 oom;需指定编码、复用实例、预建行偏移索引支持随机访问;对高性能场景可选 span 手动解析,但需谨慎处理边界与编码。

用 StreamReader 按行流式读取,别加载进内存
GB 级文件直接 File.ReadAllLines() 或 File.ReadAllText() 会瞬间 OOM——字符串对象在 .NET 中是 UTF-16 编码,1GB 的 ASCII 文件加载后实际占用约 2GB 内存,还附带 GC 压力。StreamReader 是唯一靠谱的起点,它内部使用缓冲区(默认 1024 字节),逐块解码、按需吐出字符串,内存占用稳定在几 KB 到几十 KB。
实操建议:
- 始终指定编码,如
new StreamReader(path, Encoding.UTF8),避免 bom 判断开销或乱码 - 用
ReadLine()而非ReadToEnd();若需跳过前 N 行,用循环 +DiscardBufferedData()无意义,直接for丢弃即可 - 不要在循环里反复新建
StreamReader;一个实例复用到底
需要随机访问某一行?别硬扛,先建索引
StreamReader 不支持 Seek 到第 N 行——因为行长度不固定,无法 O(1) 定位。想“读第 100 万行”,只能从头扫,耗时不可控。真有此需求,必须预处理建行偏移索引。
做法很简单:第一遍扫描只记每行起始 Stream.position,写入一个轻量二进制或 CSV 文件(如每行存一个 long);后续读取时用 FileStream.Seek() 跳转,再用 StreamReader 读该行:
using var fs = new FileStream("file.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan); fs.Seek(offsets[lineNumber], SeekOrigin.Begin); using var sr = new StreamReader(fs, Encoding.UTF8); string line = sr.ReadLine(); // 就是你要的那行
注意:FileOptions.SequentialScan 提示 OS 使用顺序读优化,对大文件有效;索引文件本身只有几 MB,远小于原始文件。
比逐行更快?试试 Span<byte></byte> + 手动解析
如果文件格式简单(如纯 ASCII 日志、CSV 无引号嵌套),且你愿意放弃部分可读性换性能,Span<byte></byte> 直接操作字节比 StreamReader 快 2–5 倍。核心是绕过字符串解码、避免 GC 分配。
关键点:
- 用
FileStream.ReadAsync(Memory<byte>)</byte>配合栈上Span<byte></byte>处理缓冲区 - 用
IndexOf((byte)'n')找行尾,Utf8Decoder.Decode()按需转字符串(仅对目标行) - 务必处理跨缓冲区的换行符(即 n 恰好在 buffer 边界),需保留末尾不完整行头
- 不推荐新手直接上手——调试困难,编码逻辑(如 UTF-8 多字节)易出错
别忽略文件系统和硬件层的影响
代码再优,遇到机械硬盘、网络共享盘(SMB/NFS)、或 NTFS 压缩属性,吞吐量可能跌到 10MB/s 以下。这些不是 C# 能解决的:
- 确认文件是否启用了 NTFS 压缩:
fsutil behavior query disablelastaccess和compact /q file.txt查看;压缩文件会强制解压到内存再读,彻底废掉流式优势 - SSD 上开启
FileOptions.RandomAccess可能反而拖慢;顺序大文件一律用SequentialScan - 远程文件优先考虑下载本地再处理;SMB 共享下
StreamReader的缓冲区大小建议调大到 64KB 或 128KB 减少往返
真正卡住的时候,先用 Process Monitor 看是不是在等磁盘 IO,而不是急着改 C# 代码。