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

如何将IEnumerable<Stream>最优合并为单个MemoryStream或byte[]

性能瓶颈分析

你现有实现的性能问题主要来自两个方面:

  • MemoryStream默认初始容量只有0,写入过程中会按当前容量翻倍的规则扩容,每次扩容都需要拷贝全量内存,总数据量越大,扩容次数越多,性能损耗越高
  • 没有自定义CopyTo的缓冲区大小,默认4KB的缓冲区对于大体积流来说,会产生过多的IO读写次数
优化方案

方案1:预分配容量(适合所有流支持读取Length的场景)

如果你的所有源流都支持CanSeek = true(即可以读取Length属性,常见的FileStream、MemoryStream都属于这类),可以提前计算总容量预分配,完全避免扩容开销:

long totalSize = 0;
// 先遍历计算总长度
foreach (var stream in streams)
{
    if (!stream.CanSeek)
    {
        throw new InvalidOperationException("存在不可定位的流,无法提前计算总容量");
    }
    totalSize += stream.Length;
}

byte[] byteData;
// 初始化时直接分配目标大小的内存
using (var ms = new MemoryStream((int)totalSize))
{
    foreach (var stream in streams)
    {
        // 如果源流当前位置不是起始位置,需要先重置
        // stream.Position = 0;
        // 自定义缓冲区大小,大体积流推荐用128KB/256KB,比默认4KB性能更好
        stream.CopyTo(ms, 1024 * 128);
    }
    byteData = ms.ToArray();
}

方案2:使用池化内存流(适合存在未知长度流的场景)

如果你的流集合包含不可定位的流(比如网络流、实时生成的流),无法提前计算总长度,可以使用微软官方提供的Microsoft.IO.RecyclableMemoryStream替换原生MemoryStream:

  • 优势:基于内存池实现,避免频繁扩容带来的内存拷贝,同时减少大对象堆(LOH)分配,大幅降低GC停顿开销
  • 使用步骤:
  1. 安装NuGet包:Microsoft.IO.RecyclableMemoryStream
  2. 实现代码:
// 全局共用一个流管理器即可,不要每次请求都创建
private static readonly RecyclableMemoryStreamManager _streamManager = new RecyclableMemoryStreamManager();

// 拼接逻辑
byte[] byteData;
using (var ms = _streamManager.GetStream())
{
    foreach (var stream in streams)
    {
        stream.CopyTo(ms, 1024 * 128);
    }
    byteData = ms.ToArray();
}

方案3:零拷贝拼接流(适合仅需读取拼接结果的场景)

如果你拿到拼接后的流只需要做单次读取操作(比如上传、写入文件等),不需要转成byte[],可以直接实现一个拼接流的包装类,完全不需要拷贝任何流内容,性能最高:

public class ConcatenatedStream : Stream
{
    private readonly IEnumerator<Stream> _streamEnumerator;
    private Stream _currentStream;

    public ConcatenatedStream(IEnumerable<Stream> streams)
    {
        _streamEnumerator = streams.GetEnumerator();
        MoveToNextStream();
    }

    private void MoveToNextStream()
    {
        if (_streamEnumerator.MoveNext())
        {
            _currentStream = _streamEnumerator.Current;
            _currentStream.Position = 0;
        }
        else
        {
            _currentStream = null;
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_currentStream == null) return 0;
        int readCount = _currentStream.Read(buffer, offset, count);
        if (readCount == 0)
        {
            MoveToNextStream();
            return Read(buffer, offset, count);
        }
        return readCount;
    }

    // 以下属性和方法按需实现,不需要的功能可直接抛出NotSupportedException
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() => throw new NotSupportedException();
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _currentStream?.Dispose();
            while (_streamEnumerator.MoveNext())
            {
                _streamEnumerator.Current.Dispose();
            }
            _streamEnumerator.Dispose();
        }
        base.Dispose(disposing);
    }
}

使用时直接实例化该类即可,不需要做任何流拷贝操作:

using var concatenatedStream = new ConcatenatedStream(streams);
// 直接读取concatenatedStream即可得到拼接后的内容
额外优化建议
  • 如果源流是MemoryStream,可以直接调用TryGetBuffer()获取底层字节段,直接写入目标流,跳过CopyTo的中间缓冲区拷贝
  • 异步场景下使用CopyToAsync替代同步CopyTo,可以大幅提升高并发下的吞吐量
  • 如果所有流的总大小超过2GB,不要用MemoryStream存储,直接写入文件流或者分片处理

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 03:51:04