【问题标题】:How do I write an obscene amount of data to file?如何将大量数据写入文件?
【发布时间】:2013-07-25 23:09:16
【问题描述】:

我正在开发一个应用程序,它从巨大的文本文件 (~2.5 GB) 中读取行,将每一行处理为特定格式,然后将每一行写入一个文本文件。关闭输出文本文件后,程序“批量插入”(SQL Server)将数据插入我的数据库。它有效,只是速度很慢。

我正在使用StreamReaderStreamWriter

由于我必须处理文本,我几乎只能一次读一行;但是,我认为如果我制作一个行集合并每隔 1000 行左右写出一个集合,它至少会加快速度。问题是(这可能纯粹是由于我的无知)我无法使用StreamWriter 编写string[]。在探索 StackOverflow 和互联网的其余部分后,我遇到了 File.WriteAllLines,它允许我将 string[]s 写入文件,但我认为我的计算机内存无法处理 2.5 GB 的数据一次存储。此外,该文件已创建、填充和关闭,因此我必须制作大量较小的文件来分解 2 GB 的文本文件,然后才能将它们插入数据库。所以我宁愿远离那个选项。

我能想到的一个 hack 工作是创建一个 StringBuilder 并使用 AppendLine 方法添加每一行以创建一个巨大的字符串。然后我可以将 StringBuilder 转换为字符串并将其写入文件。

但我的猜测已经足够了。我已经实现的方法有效,但我想知道是否有人可以提出一种将数据块写入文件的更好方法?

【问题讨论】:

    标签: c# performance optimization


    【解决方案1】:

    使用StreamWriter 有两件事可以提高输出速度。

    首先,确保输出文件与输入文件位于不同的物理磁盘上。如果输入和输出在同一个驱动器上,那么通常读取必须等待写入,而写入必须等待读取。磁盘一次只能做一件事。显然不是每个读取或写入等待,因为StreamReader 读入缓冲区并解析其中的行,而StreamWriter 写入缓冲区,然后在缓冲区结束时将其推送到磁盘满的。由于输入和输出文件位于不同的驱动器上,您的读取和写入会重叠。

    它们重叠是什么意思?操作系统通常会为您提前读取,因此它可以在您处理文件时缓冲您的文件。当您进行写入时,操作系统通常会缓冲该数据并将其延迟写入磁盘。因此,正在进行的异步处理数量有限。

    第二件事是增加缓冲区大小。 StreamReaderStreamWriter 的默认缓冲区大小为 4 KB。因此,每次 4K 读取或写入都会引发操作系统调用。而且,很可能是磁盘操作。

    如果您将缓冲区大小增加到 64K,那么您可以减少 16 倍的操作系统调用和 16 倍的磁盘操作(严格来说不是这样,但关闭)。使用 64K 缓冲区可以减少 25% 以上的 I/O 时间,而且非常简单:

    const int BufferSize = 64 * 1024;
    var reader = new StreamReader(filename, Encoding.UTF8, true, BufferSize);
    var writer = new StreamWriter(filename, Encoding.UTF8, BufferSize);
    

    这两件事将比您能做的任何事情都更快地加快您的 I/O。尝试使用StringBuilder 在内存中构建缓冲区只是不必要的工作,它会重复通过增加缓冲区大小可以实现的效果,并且做得不正确很容易使您的程序变慢。 p>

    我会提醒大家不要使用大于 64 KB 的缓冲区。在某些系统上,使用高达 256 KB 的缓冲区可以获得稍微好一点的结果,但在其他系统上,您会获得显着更差的性能——慢 50%!我从来没有见过使用大于 256 KB 的缓冲区比使用 64 KB 的缓冲区表现更好的系统。根据我的经验,64 KB 是最佳选择。

    您可以做的另一件事是使用三个线程:读取器、处理器和写入器。他们与队列通信。这可以将您的总时间从(input-time + process-time + output-time) 减少到非常接近max(input-time, process-time, output-time) 的时间。使用 .NET,真的 很容易设置。请参阅我的博文:Simple multithreading, Part 1Simple multithreading, Part 2

    【讨论】:

    • 好建议。我不知道设置缓冲区大小。我还找到了一篇很好的文章:research.microsoft.com/pubs/64538/tr-2004-136.pdf
    • @khinkle:感谢文章链接。好东西!
    • 出于好奇和缺乏经验,我想听听您对此的看法——那篇文章提到使用FileStreams 可以非常有效地创建新文件,因为我可以使用SetLength() 方法来提供估计文件有多大。它指出,“这使文件系统可以有效地预先分配物理媒体的最大可能块(最少碎片)来保存所有文件内容,从而减少碎片。”这会以某种显着的方式提高效率吗?
    • @khinkle:可能。文章指出它可以帮助减少碎片化。您必须创建FileStream,按照文章所示对其进行扩展,然后创建StreamWriter,将其传递给打开的FileStream。请注意,会有一些启动成本(创建和扩展文件),但如果您使用我提到的三线程方法,其中大部分(可能大部分甚至全部)可以同时发生,输出行流入输出缓冲区.
    • 关于多线程的绝妙建议。我不知道为什么我第一次阅读您的答案时错过了。
    【解决方案2】:

    根据docsStreamWriter默认不会在每次写入后自动刷新,所以是缓冲的。

    您还可以在 File 类上使用一些惰性方法,如下所示:

    File.WriteAllLines("output.txt", 
        File.ReadLines("filename.txt").Select(ProcessLine));
    

    ProcessLine 声明如下:

    private string ProcessLine(string input) {
        string result =         // do some calculation on input
        return result;
    }
    

    由于ReadLines 是惰性的,而WriteAllLines 具有惰性重载,它会流式传输文件而不是尝试读取整个文件。

    【讨论】:

    • +1 这真是一个优雅的解决方案。单行读取处理输出。一定会喜欢的。
    【解决方案3】:

    构建要编写的字符串怎么样?

    类似

    int cnt = 0;
    StringBuilder s = new StringBuilder();
    while(line = reader.readLine())
    {
      cnt++;
      String x = (manipulate line);
      s.append(x+"\n");
      if(cnt%10000 == 0)
      {
         StreamWriter.write(s);
         s=new StringBuilder();
      }
    }
    

    已编辑,因为下面的评论是正确的,应该使用 stringbuilder。

    【讨论】:

    • 重复字符串连接对性能非常不利,因为每次都必须分配一个新字符串。这就是 StringBuilder 存在的原因。
    • 好主意。当我在最初的问题中提议使用StringBuilder 时,我的想法也是如此。
    • 使用AppendLine 而不是Append(x+'\n')
    • 顺便说一下,while(line = reader.ReadLine()) 无法编译。你需要写while((line = reader.ReadLine()) != null)。或者使用while (!reader.EndOfStream),在循环中使用ReadLine
    猜你喜欢
    • 2013-01-23
    • 2017-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-04
    • 1970-01-01
    • 2020-08-02
    • 2013-05-08
    相关资源
    最近更新 更多