107

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

发生的情况是文件被读取并推送到 RichTextBox 中,然后导航 - 不要太担心这部分。

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

[Reader].ReadToEnd()

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

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

一些假设:

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

现在的问题:

  • 我可以简单地使用 StreamReader,然后检查 Length 属性(因此 ProgressMax)并发出读取设置的缓冲区大小并在后台工作人员内部的 while 循环中迭代这样它就不会阻塞主 UI 线程?然后在完成后将 stringbuilder 返回到主线程。
  • 内容将转到 StringBuilder。如果长度可用,我可以用流的大小初始化 StringBuilder 吗?

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

4

12 回答 12

193

您可以通过使用 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-ish 文本文件(比此处涉及的文件大得多)的代码,并通过使用生产者/消费者模式获得了显着的性能提升。生产者任务使用 读取文本行BufferedStream并将它们交给一个单独的消费者任务进行搜索。

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

为什么 BufferedStream 更快

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

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

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

有关的

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

于 2012-03-10T01:22:32.293 回答
28

如果您阅读此网站上的性能和基准统计数据,您会发现读取(因为读取、写入和处理都不同)文本文件的最快方法是以下代码片段:

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

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

于 2014-09-19T14:21:27.713 回答
16

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

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

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

于 2010-01-29T13:03:51.473 回答
8

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

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

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

于 2014-09-30T12:38:54.310 回答
8

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

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

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

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

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

StringBuilder 选项对于将系统用作单用户的人来说看起来不错,但是当您有两个或多个用户同时读取大文件时,您就会遇到问题。

于 2010-01-29T12:42:11.850 回答
6

This should be enough to get you started.

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();
    }
}
于 2010-01-29T12:56:33.113 回答
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);
        }
    }
}

来源文章

于 2010-01-29T12:52:50.243 回答
4

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

由于标准字符串只能大小为 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);
}
}

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

于 2018-01-22T05:58:25.640 回答
3

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

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

于 2010-01-29T12:52:03.043 回答
3

虽然最受好评的答案是正确的,但它缺乏多核处理的使用。就我而言,我使用 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

方法 意思是 错误 标准差 0代 第一代 第 2 代 已分配
GetTopWordsSync 33.03 秒 0.175 秒 0.155 秒 1194000 314000 7000 7.06 GB
GetTopWordsParallel 10.89 秒 0.121 秒 0.113 秒 1225000 354000 8000 7.18 GB

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

于 2021-01-21T15:59:05.663 回答
1

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

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等)时正确排列。

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

于 2010-07-09T18:35:03.297 回答
1

我的文件超过 13 GB: 在此处输入图像描述

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

读取大文本文件

更多信息

于 2018-08-18T18:40:45.773 回答