如何将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停顿开销
- 使用步骤:
- 安装NuGet包:
Microsoft.IO.RecyclableMemoryStream - 实现代码:
// 全局共用一个流管理器即可,不要每次请求都创建 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
相关产品推荐
相关产品推荐

