使用C#中的流读取大型文本文件

Nic*_*Lee 89 .net c# stream streamreader large-files

我有一个很好的任务,就是如何处理大型文件被加载到我们应用程序的脚本编辑器中(就像我们用于快速宏的内部产品的VBA一样).大多数文件大约300-400 KB,这是很好的加载.但是当它们超过100 MB时,这个过程很难(正如你所期望的那样).

发生的事情是将文件读取并推入RichTextBox然后导航 - 不要过于担心这部分.

编写初始代码的开发人员只是使用StreamReader并且正在执行

[Reader].ReadToEnd()
Run Code Online (Sandbox Code Playgroud)

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

我的任务是打破这段代码,将其以块的形式读入缓冲区并显示一个带有取消选项的进度条.

一些假设:

  • 大多数文件将是30-40 MB
  • 文件的内容是文本(非二进制),有些是Unix格式,有些是DOS.
  • 一旦检索到内容,我们就可以找出使用的终结符.
  • 一旦加载了在richtextbox中渲染所需的时间,没有人会担心.这只是文本的初始加载.

现在提问:

  • 我可以简单地使用StreamReader,然后检查Length属性(so ProgressMax)并为set缓冲区大小发出Read并在后台worker 中的while循环WHILST中迭代,这样它就不会阻塞主UI线程?然后在完成后将stringbuilder返回到主线程.
  • 内容将转到StringBuilder.如果长度可用,我可以用流的大小初始化StringBuilder吗?

这些(在您的专业意见中)是好主意吗?我过去曾经有一些问题从Streams读取内容,因为它总会错过最后几个字节或者其他东西,但如果是这样的话,我会问另一个问题.

Eri*_* J. 168

您可以使用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)
    {

    }
}
Run Code Online (Sandbox Code Playgroud)

2013年3月更新

我最近编写了用于读取和处理(搜索文本)1 GB-ish文本文件的代码(比这里涉及的文件大得多),并通过使用生产者/消费者模式实现了显着的性能提升.生产者任务使用文本行读取BufferedStream并将它们交给执行搜索的单独的消费者任务.

我用它来学习TPL数据流,这非常适合快速编码这种模式.

为什么BufferedStream更快

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

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

根据注释,FileStream应该在内部使用BufferedStream.在首次提供此答案时,我通过添加BufferedStream测量了显着的性能提升.当时我在32位平台上瞄准.NET 3.x. 今天,在64位平台上面向.NET 4.5,我没有看到任何改进.

有关

我遇到了一个案例,从ASP.Net MVC操作将大量生成的CSV文件传输到Response流非常慢.在这种情况下,添加BufferedStream可将性能提高100倍.有关更多信息,请参阅非缓冲输出非常慢

  • Dude,BufferedStream让一切变得与众不同.+1 :) (12认同)
  • 真?这在我的测试场景中没有任何区别.根据[Brad Abrams](http://blogs.msdn.com/b/brada/archive/2004/04/15/114329.aspx),在FileStream上使用BufferedStream没有任何好处. (3认同)
  • 根据以下内容,这是无用的:http://stackoverflow.com/questions/492283/when-to-use-net-bufferedstream-class FileStream已在内部使用缓冲区. (3认同)
  • 向IO子系统请求数据会产生成本。对于旋转的磁盘,您可能必须等待磁盘旋转到位才能读取下一个数据块,或更糟糕的是,等待磁盘磁头移动。尽管SSD没有机械部件来减慢速度,但是访问它们仍然需要按IO操作的成本进行。缓冲的流读取的内容不仅仅是StreamReader所请求的内容,从而减少了对OS的调用次数,并最终减少了单独的IO请求的次数。 (2认同)
  • @NickCox:您的结果可能会因基础IO子系统而异。在旋转的磁盘和磁盘控制器中,在其高速缓存中没有数据(以及Windows未高速缓存的数据)上,加速非常快。布拉德(Brad)的专栏写于2004年。我最近评估了实际的,巨大的改进。 (2认同)

小智 17

如果您阅读本网站上性能和基准测试统计数据,您将看到最快的阅读方式(因为阅读,编写和处理都不同),文本文件是以下代码片段:

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

所有大约9种不同的方法都是基准标记,但是大多数时候这个方法似乎都提前出来,甚至像其他读者所提到的那样执行缓冲读取器.

  • 这对于剥离19GB postgres文件并将其转换为多个文件中的sql语法非常有效。感谢从未正确执行我的参数的postgres家伙。/叹 (2认同)

Chr*_*ter 15

您说在加载大文件时,系统会要求您显示进度条.这是因为用户真的想要查看文件加载的确切百分比,还是因为他们想要视觉反馈才能发生某些事情?

如果后者是真的,则解决方案变得更加简单.只需reader.ReadToEnd()在后台线程上执行,并显示一个选取框类型的进度条而不是正确的进度条.

我提出这一点,因为根据我的经验,这种情况经常发生.当您编写数据处理程序时,用户肯定会对%完整数字感兴趣,但对于简单但缓慢的UI更新,他们更可能只想知道计算机没有崩溃.:-)

  • 但是,用户可以取消ReadToEnd呼叫吗? (2认同)

小智 8

对于二进制文件,我发现最快的阅读方式就是这个.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }
Run Code Online (Sandbox Code Playgroud)

在我的测试中它快了几百倍.

  • 你有这方面的确凿证据吗?OP为什么要使用它而不是任何其他答案?请深入挖掘并提供更多细节 (2认同)

Tuf*_*ufo 6

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

并尝试永远不要使用ReadToEnd().这是你认为"他们为什么要成功?"的功能之一; 这是一个脚本小子的助手,可以很好地处理小事情,但正如你所看到的那样,对于大文件来说它很糟糕......

那些告诉你使用StringBuilder的人需要更频繁地阅读MSDN:

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

这意味着巨大的内存分配,大量使用交换文件系统,模拟硬盘驱动器的部分就像RAM内存,但硬盘驱动器非常慢.

StringBuilder选项看起来很适合将系统用作单用户的用户,但是如果有两个或更多用户同时读取大文件,则会出现问题.


Jam*_*mes 5

看看下面的代码片段。你提到过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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

来源文章

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

Cha*_*ion 5

这应该足以让你入门.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我会将"var buffer = new char [1024]"移出循环:每次都没有必要创建一个新的缓冲区.只需将它放在"while(count> 0)"之前. (3认同)

Rus*_*ail 5

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

由于标准字符串只能为 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);
}
}
Run Code Online (Sandbox Code Playgroud)

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


Dan*_*l B 5

虽然获得最多支持的答案是正确的,但它缺乏多核处理的使用。就我而言,有 12 个核心,我使用 PLink:

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

值得一提的是,我在面试时遇到了一个问题,询问出现次数最多的 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);
Run Code Online (Sandbox Code Playgroud)

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

方法 意思是 错误 标准差 第0代 第一代 第2代 已分配
获取热门词同步 33.03秒 0.175秒 0.155秒 1194000 314000 7000 7.06GB
获取热门单词并行 10.89秒 0.121秒 0.113秒 1225000 354000 8000 7.18GB

正如您所看到的,性能提高了 75%。

但请注意,7Gb 会立即加载到内存中,并且由于它是一个 blob,因此会给 GC 带来太大的压力。