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

C#中低内存高效实现超大CSV文件分组多文件追加写入

低内存高效CSV分组写入方案

核心问题诊断

你内存占用过高的根本原因不是StreamWriter的缓冲区,而是同时打开的StreamWriter数量过多——每个流本身会占用固定内存,若分组数达到上千上万级,内存会直接堆涨。Flush仅清空写入缓冲区,不会释放流实例的内存。

具体解决方案

1. 限制并发打开的流数量(LRU缓存策略)

当分组数较多时,不可能长期保持所有分组的流打开。通过最近最少使用(LRU)缓存维护固定数量的StreamWriter,超过阈值就关闭最久未使用的流,需要时再重新打开追加。既控制内存占用,又避免频繁开关所有流的IO损耗。

2. 优化读取逻辑,减少内存分配

File.ReadLines会生成大量字符串,大文件处理时GC压力极大,这也是内存居高不下的隐性原因。改用FileStream配合Span<char>读取,减少不必要的字符串分配:

using var reader = new FileStream("input.csv", FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 65536, useAsync: false);
var buffer = new byte[65536];
var charBuffer = new char[65536];
var decoder = Encoding.UTF8.GetDecoder();
int bytesRead;
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
{
    int charsDecoded = decoder.GetChars(buffer, 0, bytesRead, charBuffer, 0);
    int lineStart = 0;
    for (int i = 0; i < charsDecoded; i++)
    {
        if (charBuffer[i] == '\n')
        {
            ProcessLine(charBuffer.AsSpan(lineStart, i - lineStart));
            lineStart = i + 1;
        }
    }
    // 处理文件末尾未换行的剩余内容
    if (lineStart < charsDecoded)
    {
        ProcessLine(charBuffer.AsSpan(lineStart, charsDecoded - lineStart));
    }
}

3. 实现LRU缓存管理StreamWriter

设置缓存大小(建议50-100个,可根据内存情况调整),每次获取分组Writer时:

  • 缓存存在则移至最近使用位置
  • 缓存不存在则打开新流;缓存满时关闭最久未使用的流再加入新流
public class StreamWriterCache : IDisposable
{
    private readonly int _maxCacheSize;
    private readonly Dictionary<string, (StreamWriter Writer, LinkedListNode<string> Node)> _cache;
    private readonly LinkedList<string> _lruList;
    private readonly string _outputDir;

    public StreamWriterCache(int maxCacheSize, string outputDir)
    {
        _maxCacheSize = maxCacheSize;
        _outputDir = outputDir;
        _cache = new Dictionary<string, (StreamWriter, LinkedListNode<string>)>();
        _lruList = new LinkedList<string>();
        Directory.CreateDirectory(outputDir);
    }

    public StreamWriter GetWriter(string groupKey)
    {
        if (_cache.TryGetValue(groupKey, out var entry))
        {
            _lruList.Remove(entry.Node);
            _lruList.AddFirst(entry.Node);
            return entry.Writer;
        }

        // 缓存满时清理最久未使用的流
        while (_cache.Count >= _maxCacheSize)
        {
            var leastUsedKey = _lruList.Last.Value;
            _cache[leastUsedKey].Writer.Dispose();
            _cache.Remove(leastUsedKey);
            _lruList.RemoveLast();
        }

        // 创建带大缓冲区的StreamWriter,减少IO次数
        var filePath = Path.Combine(_outputDir, $"{groupKey}.csv");
        var writer = new StreamWriter(filePath, true, Encoding.UTF8, bufferSize: 65536);
        var node = _lruList.AddFirst(groupKey);
        _cache.Add(groupKey, (writer, node));
        return writer;
    }

    public void Dispose()
    {
        foreach (var entry in _cache.Values)
        {
            entry.Writer.Dispose();
        }
        _cache.Clear();
        _lruList.Clear();
    }
}

4. 行处理与分组写入

在ProcessLine方法中解析分组标识,从缓存获取Writer写入:

private static readonly StreamWriterCache _writerCache = new StreamWriterCache(100, "./output");

private static void ProcessLine(ReadOnlySpan<char> line)
{
    // 按CSV格式解析分组标识(示例为取第一个逗号前的内容)
    int separatorIdx = line.IndexOf(',');
    if (separatorIdx == -1) return;
    var groupKey = line.Slice(0, separatorIdx).ToString();

    var writer = _writerCache.GetWriter(groupKey);
    writer.WriteLine(line.ToString());
}

额外优化建议

  • 调整缓冲区大小:读取和写入缓冲区设为64KB或128KB(根据磁盘性能调整),适配大文件IO场景。
  • 禁用手动Flush:保留StreamWriter默认的缓冲策略,满缓冲区再写入,减少IO次数。
  • 使用同步IO:大文件处理时同步IO比异步更稳定,避免线程切换开销。
  • 预分配文件空间:若预先知晓所有分组,可先创建空文件并预分配磁盘空间,减少碎片化带来的IO延迟。
  • 监控GC:用性能工具查看内存分配,尽量用Span处理文本,减少临时字符串的生成。

效果预期

该方案处理5GB文件时,内存占用可控制在500MB以内(取决于缓存大小),IO效率能满足1小时内完成的要求。若分组数极多(上万级),可适当调小缓存大小进一步降低内存占用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 04:10:32