You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何直接逐个读取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延迟被彻底暴露。

优化方案

  1. 保留CopyTo+MemoryStream方案:这是已经验证的高效方案,只要内存占用在可接受范围内,直接使用即可。
  2. 优化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("输入流长度异常,可能格式错误。");
    }
    
  3. 预加载文件到内存再解压:先将FileStream内容读取到MemoryStream,再创建DeflateStream读取,本质和CopyTo方案类似,但可以提前自定义缓冲区大小。

内容的提问来源于stack exchange,提问作者Bolpat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 23:44:55