【问题标题】:Why can't I leverage 4GB of RAM in my computer to process less than 2GB of information in C#?为什么我不能利用计算机中的 4GB RAM 来处理 C# 中少于 2GB 的信息?
【发布时间】:2010-10-16 19:16:33
【问题描述】:

场景:我需要以数学方式处理超过 1.5GB 的文本和 csv 文件。我尝试使用 SQL Server Express,但加载信息,即使使用 BULK 导入也需要很长时间,理想情况下我需要将整个数据集放在内存中,以减少硬盘 IO。

有超过 120,000,000 条记录,但即使我尝试将信息过滤到仅一列(内存中),我的 C# 控制台应用程序也消耗约 3.5GB 的内存来处理仅 125MB(实际读入 700MB)文本。

GC 似乎没有收集对字符串和字符串数组的引用,即使在将所有引用设置为 null 并使用 using 关键字封装 IDisposables 之后也是如此。

我认为罪魁祸首是 String.Split() 方法,它为每个逗号分隔值创建一个新字符串。

您可能会建议我什至不应该将不需要的*列读入字符串数组,但这没有抓住重点:如何将这个 整个 数据集放在内存中,以便我可以处理它在 C# 中是并行的吗?

我可以优化统计算法并使用复杂的调度算法协调任务,但这是我在遇到内存问题之前希望做的事情,而不是因为。

我已经包含一个完整的控制台应用程序,它可以模拟我的环境,应该有助于重现问题。

感谢任何帮助。提前致谢。

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace InMemProcessingLeak
{
    class Program
    {
        static void Main(string[] args)
        {
            //Setup Test Environment. Uncomment Once
            //15000-20000 files would be more realistic
            //InMemoryProcessingLeak.GenerateTestDirectoryFilesAndColumns(3000, 3);
            //GC
            GC.Collect();
            //Demostrate Large Object Memory Allocation Problem (LOMAP)
            InMemoryProcessingLeak.SelectColumnFromAllFiles(3000, 2);
        }
    }

    class InMemoryProcessingLeak
    {
        public static List<string> SelectColumnFromAllFiles(int filesToSelect, int column)
        {
            List<string> allItems = new List<string>();
            int fileCount = filesToSelect;
            long fileSize, totalReadSize = 0;

            for (int i = 1; i <= fileCount; i++)
            {
                allItems.AddRange(SelectColumn(i, column, out fileSize));
                totalReadSize += fileSize;
                Console.Clear();
                Console.Out.WriteLine("Reading file {0:00000} of {1}", i, fileCount);
                Console.Out.WriteLine("Memory = {0}MB", GC.GetTotalMemory(false) / 1048576);
                Console.Out.WriteLine("Total Read = {0}MB", totalReadSize / 1048576);
            }
            Console.ReadLine();
            return allItems;

        }

        //reads a csv file and returns the values for a selected column
        private static List<string> SelectColumn(int fileNumber, int column, out long fileSize)
        {
            string fileIn;
            FileInfo file = new FileInfo(string.Format(@"MemLeakTestFiles/File{0:00000}.txt", fileNumber));
            fileSize = file.Length;
            using (System.IO.FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                using (System.IO.StreamReader sr = new System.IO.StreamReader(fs))
                {
                    fileIn = sr.ReadToEnd();
                }
            }

            string[] lineDelimiter = { "\n" };
            string[] allLines = fileIn.Split(lineDelimiter, StringSplitOptions.None);

            List<string> processedColumn = new List<string>();

            string current;
            for (int i = 0; i < allLines.Length - 1; i++)
            {
                current = GetColumnFromProcessedRow(allLines[i], column);
                processedColumn.Add(current);
            }

            for (int i = 0; i < lineDelimiter.Length; i++) //GC
            {
                lineDelimiter[i] = null;
            }
            lineDelimiter = null;

            for (int i = 0; i < allLines.Length; i++) //GC
            {
                allLines[i] = null;
            }
            allLines = null;
            current = null;

            return processedColumn;
        }

        //returns a row value from the selected comma separated string and column position
        private static string GetColumnFromProcessedRow(string line, int columnPosition)
        {
            string[] entireRow = line.Split(",".ToCharArray());
            string currentColumn = entireRow[columnPosition];
            //GC
            for (int i = 0; i < entireRow.Length; i++)
            {
                entireRow[i] = null;
            }
            entireRow = null;
            return currentColumn;
        }

        #region Generators
        public static void GenerateTestDirectoryFilesAndColumns(int filesToGenerate, int columnsToGenerate)
        {
            DirectoryInfo dirInfo = new DirectoryInfo("MemLeakTestFiles");
            if (!dirInfo.Exists)
            {
                dirInfo.Create();
            }
            Random seed = new Random();

            string[] columns = new string[columnsToGenerate];

            StringBuilder sb = new StringBuilder();
            for (int i = 1; i <= filesToGenerate; i++)
            {
                int rows = seed.Next(10, 8000);
                for (int j = 0; j < rows; j++)
                {
                    sb.Append(GenerateRow(seed, columnsToGenerate));
                }
                using (TextWriter tw = new StreamWriter(String.Format(@"{0}/File{1:00000}.txt", dirInfo, i)))
                {
                    tw.Write(sb.ToString());
                    tw.Flush();
                }
                sb.Remove(0, sb.Length);
                Console.Clear();
                Console.Out.WriteLine("Generating file {0:00000} of {1}", i, filesToGenerate);
            }
        }

        private static string GenerateString(Random seed)
        {
            StringBuilder sb = new StringBuilder();
            int characters = seed.Next(4, 12);
            for (int i = 0; i < characters; i++)
            {
                sb.Append(Convert.ToChar(Convert.ToInt32(Math.Floor(26 * seed.NextDouble() + 65))));
            }
            return sb.ToString();
        }

        private static string GenerateRow(Random seed, int columnsToGenerate)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(seed.Next());
            for (int i = 0; i < columnsToGenerate - 1; i++)
            {
                sb.Append(",");
                sb.Append(GenerateString(seed));
            }
            sb.Append("\n");

            return sb.ToString();
        }
        #endregion
    }
}

*在程序的整个生命周期中,这些其他列将被需要并按顺序和随机访问,因此每次从磁盘读取都是非常繁重的开销。

**环境说明:4GB DDR2 SDRAM 800、Core 2 Duo 2.5Ghz、.NET Runtime 3.5 SP1、Vista 64。

【问题讨论】:

  • 除了下面的答案,我注意到您使用了基于数组的 List。据我所知,每次达到当前容量时,数组大小都会翻倍。因此,一旦达到特定限制,这可能真的很麻烦。

标签: c# performance memory string garbage-collection


【解决方案1】:

我建议逐行阅读,而不是整个文件,或者最多 1-2mb 的块。

更新:
在 Jon 的 cmets 中,我很好奇并尝试了 4 种方法:

  • StreamReader.ReadLine(默认和自定义缓冲区大小),
  • StreamReader.ReadToEnd
  • 上面列出了我的方法。

读取 180mb 的日志文件:

  • ReadLine 毫秒:1937
  • ReadLine 更大的缓冲区,ascii ms: 1926
  • ReadToEnd 毫秒:2151
  • 自定义毫秒:1415

自定义 StreamReader 是:

StreamReader streamReader = new StreamReader(fileStream, Encoding.Default, false, 16384)

StreamReader 的缓冲区默认为 1024。

对于内存消耗(实际问题!) - 使用了约 800mb。而且我给出的方法仍然使用StringBuilder(它使用字符串)所以不会减少内存消耗。

【讨论】:

  • (我还强烈建议使用“using”语句来避免在出现异常时保持流打开,并将“bytesRead”重命名为“charactersRead”。)
  • 我会编辑我的答案,因为我自相矛盾 + 用你的建议更新那个 3 岁的代码。 16384 缓冲区大小是主要区别,这来自 microsoft.public.dotnet.languages.csharp 关于 C++ 与 C# 文本大小性能的讨论。
  • TextReader 和 StreamReader 逐字节执行 iirc,当我对读取 1.5mb 日志文件进行一些测试时,速度要慢得多——我也一次解析每一行
  • TextReader.ReadLine() 逐个字符地检查行尾字符 - 但它不会一次调用读取单个字符。 (而且 StreamReader 和 FileStream 都有缓冲区。)顺便说一句,使用 File.OpenText 对它创建的 FileStream 应用了一些优化,特别是(续)
  • 它针对顺序访问进行了优化。 (我最近一直在对 IO 和 CPU 的混合进行基准测试 - 请参阅 msmvps.com/jon.skeet 和最后几篇文章)
【解决方案2】:

现代 GC 语言利用大量廉价 RAM 来卸载内存管理任务。这会带来一定的开销,但您的典型业务应用程序实际上并不需要那么多信息。许多程序只使用不到一千个对象。手动管理这么多是一件苦差事,甚至每个对象的一千字节开销都无关紧要。

在您的情况下,每个对象的开销正在成为一个问题。例如,您可以考虑将每一列表示为一个对象,使用单个字符串和整数偏移量数组实现。要返回单个字段,请返回一个子字符串(可能作为 shim)

【讨论】:

  • 似乎我已经用尽了可用的 C# 最佳实践,而您的回答指出了下一个最佳实践。我真的很喜欢 C#,但我想知道如果我将来遇到类似这样的其他数据密集型挑战,学习和使用 C++/CLI 是否是一个好主意。
  • 考虑原生 C++;在这些情况下,它可能非常有效。是的,您必须为 C# 中包含的功能编写大量代码。但这正是重点;您是少数负担不起 .Net 默认值的人之一。
  • 几年前我在尝试使用一次性 .net DB 转换实用程序时遇到了一个非常相似的问题。我无法让 .net 快速运行,但一个非常简单的 c++ OLEDB 应用程序运行得非常快。我认为我使用的 .net lib 内存效率非常低。
  • 差异从 10 小时缩短到 10 分钟(大约)。我很感激我可能已经把它翘起来了,但我尝试了我能想到的一切来让它更快地工作。有时你只是需要一个更好的工具来完成一些工作。
【解决方案3】:

是的,String.Split 为每个“片段”创建一个新的 String 对象——这就是它的本意。

现在,请记住 .NET 中的字符串是 Unicode(实际上是 UTF-16),加上对象开销,字符串的字节成本大约是 20 + 2*n 其中n 是字符数。

这意味着如果您有很多小字符串,与所涉及的文本数据的大小相比,它会占用大量内存。例如,将 80 个字符的行拆分为 10 x 8 个字符串将占用文件中的 80 个字节,但 10 * (20 + 2*8) = 360 个字节的内存 - 4.5 倍的爆炸!

我怀疑这是一个 GC 问题 - 我建议您在不需要时删除将变量设置为 null 的额外语句 - 只是数据过多的问题。

建议您逐行阅读文件(使用TextReader.ReadLine() 而不是TextReader.ReadToEnd())。如果不需要,显然将整个文件保存在内存中是一种浪费。

【讨论】:

  • 非常丰富的答案。正如 MSalters 所建议的那样,如果我想一次处理所有信息,我似乎需要以不同的方式表示数据。
  • 是的——尽管你最终还是会遇到问题。如果您能找到一种以流方式处理数据的方法,那么该解决方案的扩展性会好很多。
  • 您会推荐类似“push”linq 的东西,这样我就可以在不循环的情况下跨文件提取关系信息吗?
  • 这完全取决于您需要做什么,但是是的,Push LINQ 非常适合聚合庞大的数据集。
猜你喜欢
  • 1970-01-01
  • 2013-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 2014-09-12
  • 2012-12-21
相关资源
最近更新 更多