为何直接逐个读取DeflateStream的速度如此缓慢?
问题:直接读取DeflateStream速度远慢于先复制到MemoryStream再读取
我有一个可读写自定义二进制格式数据的类,Windows Forms应用启动时需要读取该格式文件。因为格式冗余度高,我用DeflateStream做数据压缩,压缩效果不错。
但实际应用里读取速度特别慢(6500–7200毫秒),功能测试中读取同一个文件却只需要185–200毫秒,读取逻辑完全一致。测试发现,把整个DeflateStream复制到MemoryStream后再读取(330–380毫秒),比直接读DeflateStream快太多。
慢读取实现
System.Diagnostics.Stopwatch sw = new(); Tally tally = new(); string fileName = "data.dat"; using FileStream file = new(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); sw.Start(); try { using DeflateStream ds = new(file, CompressionMode.Decompress, leaveOpen: true); tally.Read(ds); // 速度慢 } finally { sw.Stop(); MessageBox.Show($"读取完成(条目数 = {tally.Count})耗时 {sw.ElapsedMilliseconds} 毫秒。"); }
快读取实现
System.Diagnostics.Stopwatch sw = new(); Tally tally = new(); string fileName = "data.dat"; using FileStream file = new(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); sw.Start(); try { using DeflateStream ds = new(file, CompressionMode.Decompress, leaveOpen: true); using MemoryStream ms = new(); ds.CopyTo(ms); ms.Seek(0, SeekOrigin.Begin); tally.Read(ms); // 速度快 } finally { sw.Stop(); MessageBox.Show($"读取完成(条目数 = {tally.Count})耗时 {sw.ElapsedMilliseconds} 毫秒。"); }
两者性能差距极大,我搞不懂问题出在哪。
Read方法实现
SortedList<TallyKey, TallyValue> m_Results = new(); public void Read(Stream stream) { ArgumentNullException.ThrowIfNull(stream); m_Results.Clear(); { Span<byte> sizeBuffer = stackalloc byte[sizeof(int)]; stream.ReadExactly(sizeBuffer); m_Results.Capacity = BinaryPrimitives.ReadInt32LittleEndian(sizeBuffer); } Span<byte> buffer = stackalloc byte[EntrySize]; int bytesRead = 0; for (int n; (n = stream.Read(buffer[bytesRead..])) is not 0; ) { if ((bytesRead += n) < EntrySize) continue; bytesRead = 0; // FromLittleEndianData 通过多次BinaryPrimitives读取创建TallyKey/TallyValue; // 每个都是小型结构体(5个int // TimeSpan + int)。 // TallyKey.SerializedSize = 7(字节) // TallyValue.SerializedSize = 8(字节) // EntrySize = TallyKey.SerializedSize + TallyValue.SerializedSize m_Results.Add( TallyKey.FromLittleEndianData(buffer[..TallyKey.SerializedSize]), TallyValue.FromLittleEndianData(buffer[TallyKey.SerializedSize..]) ); } if (bytesRead is not 0) throw new InvalidDataException( message: "输入流长度异常,可能格式错误。" ); }
原因分析
直接读取DeflateStream速度慢的核心问题是高频次的小批量读取操作:
- DeflateStream是流式解压,每次调用
stream.Read时,它需要从底层FileStream读取压缩数据、解压出最多15字节(EntrySize)的内容,这个过程会频繁触发IO操作和解压计算,上下文切换与重复初始化的开销被无限放大。 CopyTo方法默认使用81920字节的大缓冲区批量读取和解压,一次性把所有数据加载到MemoryStream,后续读取是纯内存操作,完全规避了频繁IO与小批量解压的开销。- 功能测试中速度快,大概率是因为测试环境下文件已被操作系统缓存到内存,直接读取DeflateStream时的IO开销被抵消,但生产环境启动时文件通常不在缓存,IO延迟被彻底暴露。
优化方案
- 保留CopyTo+MemoryStream方案:这是已经验证的高效方案,只要内存占用在可接受范围内,直接使用即可。
- 优化Read方法的读取粒度:改用大缓冲区批量读取解压后的数据,减少DeflateStream的调用次数,示例代码如下:
public void Read(Stream stream) { ArgumentNullException.ThrowIfNull(stream); m_Results.Clear(); Span<byte> sizeBuffer = stackalloc byte[sizeof(int)]; stream.ReadExactly(sizeBuffer); int capacity = BinaryPrimitives.ReadInt32LittleEndian(sizeBuffer); m_Results.Capacity = capacity; int entrySize = TallyKey.SerializedSize + TallyValue.SerializedSize; // 使用4KB大缓冲区,可根据实际情况调整 byte[] largeBuffer = new byte[4096]; int totalProcessed = 0; int leftoverBytes = 0; while (totalProcessed < capacity * entrySize) { int readBytes = stream.Read(largeBuffer, leftoverBytes, largeBuffer.Length - leftoverBytes); if (readBytes == 0) break; int totalAvailable = leftoverBytes + readBytes; int fullEntries = totalAvailable / entrySize; // 处理完整条目 for (int i = 0; i < fullEntries; i++) { int offset = i * entrySize; Span<byte> entrySpan = largeBuffer.AsSpan(offset, entrySize); m_Results.Add( TallyKey.FromLittleEndianData(entrySpan[..TallyKey.SerializedSize]), TallyValue.FromLittleEndianData(entrySpan[TallyKey.SerializedSize..]) ); totalProcessed += entrySize; } // 处理剩余不足一条目的字节 leftoverBytes = totalAvailable % entrySize; if (leftoverBytes > 0) { Array.Copy(largeBuffer, fullEntries * entrySize, largeBuffer, 0, leftoverBytes); } } if (totalProcessed != capacity * entrySize || leftoverBytes != 0) throw new InvalidDataException("输入流长度异常,可能格式错误。"); } - 预加载文件到内存再解压:先将FileStream内容读取到MemoryStream,再创建DeflateStream读取,本质和CopyTo方案类似,但可以提前自定义缓冲区大小。
内容的提问来源于stack exchange,提问作者Bolpat
相关产品推荐
相关产品推荐

