【问题标题】:Reading large text files with streams in C#在 C# 中使用流读取大型文本文件
【发布时间】:2011-01-10 20:07:52
【问题描述】:

我有一个可爱的任务,就是研究如何处理加载到我们应用程序的脚本编辑器中的大文件(就像我们内部产品的快速宏的 VBA)。大多数文件大约为 300-400 KB,可以很好地加载。但是当它们超过 100 MB 时,这个过程就会变得很困难(正如您所期望的那样)。

发生的情况是文件被读取并推入一个 RichTextBox,然后进行导航 - 不要太担心这部分。

编写初始代码的开发人员只是使用 StreamReader 并在做

[Reader].ReadToEnd()

这可能需要很长时间才能完成。

我的任务是分解这段代码,将其分块读取到缓冲区中,并显示一个进度条,其中包含取消它的选项。

一些假设:

  • 大多数文件大小为 30-40 MB
  • 文件的内容是文本(不是二进制),有些是 Unix 格式,有些是 DOS。
  • 检索到内容后,我们会计算出使用的终止符。
  • 一旦加载,它在richtextbox 中呈现所花费的时间就没有人关心了。这只是文本的初始加载。

现在回答问题:

  • 我是否可以简单地使用 StreamReader,然后检查 Length 属性(即 ProgressMax)并发出读取设置的缓冲区大小,并在后台工作程序内的 while 循环中迭代 WHILST,所以它不会'不阻塞主 UI 线程?完成后将字符串生成器返回到主线程。
  • 内容将转到 StringBuilder。如果长度可用,我可以用流的大小初始化 StringBuilder 吗?

这些(在您的专业意见中)是好主意吗?过去我在从 Streams 读取内容时遇到了一些问题,因为它总是会丢失最后几个字节或其他内容,但如果是这种情况,我会问另一个问题。

【问题讨论】:

  • 30-40MB 脚本文件?圣鲭鱼!我不想不得不对代码进行审查......
  • 我知道这个问题已经很老了,但前几天我发现了它并测试了 MemoryMappedFile 的推荐,这是最快的方法。比较是通过 readline 方法读取 7,616,939 行 345MB 文件在我的机器上需要 12 多个小时,而通过 MemoryMappedFile 执行相同的加载和读取需要 3 秒。
  • 这只是几行代码。请参阅我用来读取 25gb 和更大文件的这个库。 github.com/Agenty/FileReader

标签: c# .net stream streamreader large-files


【解决方案1】:

虽然最受好评的答案是正确的,但它缺乏多核处理的使用。就我而言,我使用 PLink 有 12 个内核:

Parallel.ForEach(
    File.ReadLines(filename), //returns IEumberable<string>: lazy-loading
    new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
    (line, state, index) =>
    {
        //process line value
    }
);

值得一提的是,我得到了一个面试问题,要求返回前 10 名出现次数最多的问题:

var result = new ConcurrentDictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
Parallel.ForEach(
    File.ReadLines(filename),
    new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
    (line, state, index) =>
    {
        result.AddOrUpdate(line, 1, (key, val) => val + 1);        
    }
);

return result
    .OrderByDescending(x => x.Value)
    .Take(10)
    .Select(x => x.Value);

Benchmarking: BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores [Host] : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT DefaultJob : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
GetTopWordsSync 33.03 s 0.175 s 0.155 s 1194000 314000 7000 7.06 GB
GetTopWordsParallel 10.89 s 0.121 s 0.113 s 1225000 354000 8000 7.18 GB

如您所见,它的性能提高了 75%。

【讨论】:

    【解决方案2】:

    我的文件超过 13 GB:

    以下链接包含轻松读取文件的代码:

    Read a large text file

    More information

    【讨论】:

      【解决方案3】:

      所有优秀的答案!但是,对于寻找答案的人来说,这些似乎有些不完整。

      作为标准字符串只能大小为 X,2Gb 到 4Gb,具体取决于您的配置,这些答案并不能真正满足 OP 的问题。一种方法是使用字符串列表:

      List<string> Words = new List<string>();
      
      using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
      {
      
      string line = string.Empty;
      
      while ((line = sr.ReadLine()) != null)
      {
          Words.Add(line);
      }
      }
      

      有些人可能希望在处理时对行进行标记化和拆分。字符串列表现在可以包含非常大量的文本。

      【讨论】:

        【解决方案4】:

        您最好使用内存映射文件处理here.. 内存映射文件支持将在 .NET 4 中出现(我想...我从其他人那里听说过),因此这个包装器使用 p/invokes 来做同样的工作..

        编辑:请参阅此处的 MSDN 了解其工作原理,这是 blog 条目,说明它在即将发布的 .NET 4 中是如何完成的。我之前给出的链接是围绕 pinvoke 的包装器来实现这一点。您可以将整个文件映射到内存中,并在滚动文件时像滑动窗口一样查看。

        【讨论】:

          【解决方案5】:

          如果您阅读performance and benchmark stats on this website,您会发现读取(因为读取、写入和处理都不同)文本文件的最快方法是以下sn-p代码:

          using (StreamReader sr = File.OpenText(fileName))
          {
              string s = String.Empty;
              while ((s = sr.ReadLine()) != null)
              {
                  //do your stuff here
              }
          }
          

          所有大约 9 种不同的方法都进行了基准测试,但大多数时候似乎有一种方法领先,甚至像其他读者提到的那样执行缓冲阅读器

          【讨论】:

          • 这对于剥离 19GB 的 postgres 文件以将其转换为多个文件中的 sql 语法非常有效。感谢从未正确执行我的参数的 postgres 家伙。 /叹息
          • 这里的性能差异似乎可以为非常大的文件带来回报,比如大于 150MB(你也应该使用 StringBuilder 将它们加载到内存中,加载速度更快,因为它不会产生每次添加字符时都有新字符串)
          【解决方案6】:

          您可以通过使用 BufferedStream 来提高读取速度,如下所示:

          using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
          using (BufferedStream bs = new BufferedStream(fs))
          using (StreamReader sr = new StreamReader(bs))
          {
              string line;
              while ((line = sr.ReadLine()) != null)
              {
          
              }
          }
          

          2013 年 3 月更新

          我最近编写了用于读取和处理(在其中搜索文本)1 GB 左右的文本文件(比此处涉及的文件大得多)的代码,并通过使用生产者/消费者模式获得了显着的性能提升。生产者任务使用 BufferedStream 读取文本行,并将它们交给一个单独的消费者任务进行搜索。

          我借此机会学习了 TPL 数据流,它非常适合快速编码这种模式。

          为什么 BufferedStream 更快

          缓冲区是内存中用于缓存数据的字节块,从而减少了对操作系统的调用次数。缓冲区提高了读写性能。缓冲区可以用于读取或写入,但不能同时用于两者。 BufferedStream 的 Read 和 Write 方法会自动维护缓冲区。

          2014 年 12 月更新:您的里程可能会有所不同

          基于 cmets,FileStream 应该在内部使用 BufferedStream。在首次提供此答案时,我通过添加 BufferedStream 测量了显着的性能提升。当时我的目标是 32 位平台上的 .NET 3.x。今天,针对 64 位平台上的 .NET 4.5,我没有看到任何改进。

          相关

          我遇到了一个案例,其中从 ASP.Net MVC 操作将生成的大型 CSV 文件流式传输到响应流非常慢。在这种情况下,添加 BufferedStream 将性能提高了 100 倍。更多内容见Unbuffered Output Very Slow

          【讨论】:

          • 老兄,BufferedStream 与众不同。 +1 :)
          • 从 IO 子系统请求数据是有成本的。在旋转磁盘的情况下,您可能必须等待盘片旋转到位才能读取下一个数据块,或者更糟的是,等待磁盘磁头移动。虽然 SSD 没有机械部件来减慢速度,但访问它们仍然需要每个 IO 操作成本。缓冲流读取的不仅仅是 StreamReader 请求的内容,从而减少了对操作系统的调用次数,并最终减少了单独 IO 请求的数量。
          • 真的吗?这对我的测试场景没有影响。根据Brad Abrams 的说法,在 FileStream 上使用 BufferedStream 没有任何好处。
          • @NickCox:您的结果可能会因您的底层 IO 子系统而异。在旋转磁盘和缓存中没有数据(以及 Windows 未缓存的数据)的磁盘控制器上,加速是巨大的。 Brad 的专栏写于 2004 年。我最近测量了实际的、显着的改进。
          • 这没用,根据:stackoverflow.com/questions/492283/… FileStream 内部已经使用了缓冲区。
          【解决方案7】:

          对于二进制文件,我发现最快的读取方式是这样的。

           MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
           MemoryMappedViewStream mms = mmf.CreateViewStream();
           using (BinaryReader b = new BinaryReader(mms))
           {
           }
          

          在我的测试中,它快了数百倍。

          【讨论】:

          • 你有任何确凿的证据吗?为什么 OP 应该在任何其他答案上使用它?请深入挖掘并提供更多细节
          【解决方案8】:

          迭代器可能非常适合这种类型的工作:

          public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
          {
              const int charBufferSize = 4096;
              using (FileStream fs = File.OpenRead(filename))
              {
                  using (BinaryReader br = new BinaryReader(fs))
                  {
                      long length = fs.Length;
                      int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
                      double iter = 100 / Convert.ToDouble(numberOfChunks);
                      double currentIter = 0;
                      yield return Convert.ToInt32(currentIter);
                      while (true)
                      {
                          char[] buffer = br.ReadChars(charBufferSize);
                          if (buffer.Length == 0) break;
                          stringData.Append(buffer);
                          currentIter += iter;
                          yield return Convert.ToInt32(currentIter);
                      }
                  }
              }
          }
          

          您可以使用以下方式调用它:

          string filename = "C:\\myfile.txt";
          StringBuilder sb = new StringBuilder();
          foreach (int progress in LoadFileWithProgress(filename, sb))
          {
              // Update your progress counter here!
          }
          string fileData = sb.ToString();
          

          随着文件的加载,迭代器将返回从 0 到 100 的进度号,您可以使用它来更新进度条。循环完成后,StringBuilder 将包含文本文件的内容。

          另外,由于您需要文本,我们可以只使用 BinaryReader 读取字符,这将确保您的缓冲区在读取任何多字节字符(UTF-8UTF-16 等)时正确排列。

          这一切都是在不使用后台任务、线程或复杂的自定义状态机的情况下完成的。

          【讨论】:

            【解决方案9】:

            看看下面的代码sn-p。你提到了Most files will be 30-40 MB。这声称在英特尔四核上可在 1.4 秒内读取 180 MB:

            private int _bufferSize = 16384;
            
            private void ReadFile(string filename)
            {
                StringBuilder stringBuilder = new StringBuilder();
                FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);
            
                using (StreamReader streamReader = new StreamReader(fileStream))
                {
                    char[] fileContents = new char[_bufferSize];
                    int charsRead = streamReader.Read(fileContents, 0, _bufferSize);
            
                    // Can't do much with 0 bytes
                    if (charsRead == 0)
                        throw new Exception("File is 0 bytes");
            
                    while (charsRead > 0)
                    {
                        stringBuilder.Append(fileContents);
                        charsRead = streamReader.Read(fileContents, 0, _bufferSize);
                    }
                }
            }
            

            Original Article

            【讨论】:

            • 这类测试是出了名的不可靠。重复测试时,您将从文件系统缓存中读取数据。这至少比从磁盘读取数据的实际测试快一个数量级。一个 180 MB 的文件不可能花费不到 3 秒的时间。重启你的机器,对实数运行一次测试。
            • 行 stringBuilder.Append 有潜在危险,您需要将其替换为 stringBuilder.Append( fileContents, 0, charsRead );以确保即使流提前结束,您也不会添加完整的 1024 个字符。
            • @JohannesRudolph,您的评论刚刚解决了我的一个错误。你是怎么想出 1024 这个数字的?
            【解决方案10】:

            使用后台工作程序并仅读取有限数量的行。仅在用户滚动时阅读更多内容。

            并且尽量不要使用 ReadToEnd()。这是您认为“他们为什么制作它?”的功能之一;这是一个 script kiddies' 助手,可以很好地处理小事情,但正如你所见,它对于大文件很糟糕......

            那些告诉你使用 StringBuilder 的人需要经常阅读 MSDN:

            性能注意事项
            Concat 和 AppendFormat 方法都将新数据连接到现有的 String 或 StringBuilder 对象。字符串对象连接操作总是从现有字符串和新数据创建一个新对象。 StringBuilder 对象维护一个缓冲区来容纳新数据的连接。如果空间可用,则将新数据附加到缓冲区的末尾;否则,分配一个新的更大的缓冲区,将原始缓冲区中的数据复制到新缓冲区,然后将新数据附加到新缓冲区。 String 或 StringBuilder 对象的连接操作的性能取决于内存分配发生的频率。
            String 连接操作总是分配内存,而 StringBuilder 连接操作仅在 StringBuilder 对象缓冲区太小而无法容纳新数据时才分配内存。因此,如果串联固定数量的 String 对象,则 String 类更适合串联操作。在这种情况下,编译器甚至可以将各个串联操作组合成单个操作。如果串联任意数量的字符串,则 StringBuilder 对象更适合串联操作;例如,如果一个循环连接随机数量的用户输入字符串。

            这意味着巨大的内存分配,这成为交换文件系统的大量使用,它模拟硬盘驱动器的某些部分来充当 RAM 内存,但硬盘驱动器非常慢。

            StringBuilder 选项对于以单用户身份使用系统的人来说看起来不错,但是当您有两个或多个用户同时读取大文件时,您就有问题了。

            【讨论】:

            • 远远的你们超级快!不幸的是,由于宏的工作方式,需要加载整个流。正如我提到的,不要担心富文本部分。它是我们想要改进的初始加载。
            • 所以你可以分部分工作,阅读前 X 行,应用宏,阅读第二 X 行,应用宏,等等...如果你解释这个宏的作用,我们可以帮助您更精确
            【解决方案11】:

            这应该足以让您入门。

            class Program
            {        
                static void Main(String[] args)
                {
                    const int bufferSize = 1024;
            
                    var sb = new StringBuilder();
                    var buffer = new Char[bufferSize];
                    var length = 0L;
                    var totalRead = 0L;
                    var count = bufferSize; 
            
                    using (var sr = new StreamReader(@"C:\Temp\file.txt"))
                    {
                        length = sr.BaseStream.Length;               
                        while (count > 0)
                        {                    
                            count = sr.Read(buffer, 0, bufferSize);
                            sb.Append(buffer, 0, count);
                            totalRead += count;
                        }                
                    }
            
                    Console.ReadKey();
                }
            }
            

            【讨论】:

            • 我会将“var buffer = new char[1024]”移出循环:不必每次都创建新缓冲区。只需将其放在“while (count > 0)”之前即可。
            【解决方案12】:

            您说您在加载大文件时被要求显示进度条。那是因为用户真的想看到文件加载的确切百分比,还是只是因为他们想要视觉反馈来表明正在发生的事情?

            如果后者是真的,那么解决方案就变得简单多了。只需在后台线程上执行reader.ReadToEnd(),并显示一个选框式进度条而不是正确的进度条。

            我提出这一点是因为根据我的经验,这种情况经常发生。当你在写一个数据处理程序时,那么用户肯定会对百分比完成图感兴趣,但是对于简单但缓慢的 UI 更新,他们更有可能只是想知道计算机没有崩溃。 :-)

            【讨论】:

            • 但是用户可以取消 ReadToEnd 调用吗?
            • @Tim,很好发现。在这种情况下,我们回到StreamReader 循环。但是,它仍然会更简单,因为无需提前阅读来计算进度指示器。
            猜你喜欢
            • 2021-12-05
            • 2021-07-20
            • 1970-01-01
            • 2016-07-27
            • 2015-12-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多