4

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

我正在使用StreamReaderStreamWriter

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

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

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

4

3 回答 3

11

有两件事会提高使用StreamWriter.

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

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

第二件事是增加缓冲区大小。StreamReader和的默认缓冲区大小为StreamWriter4 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只是不必要的工作,它在复制通过增加缓冲区大小可以实现的目标方面做得很糟糕,并且做得不正确很容易让你的程序变慢

我会警告缓冲区大小不要超过 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,设置起来非常容易。请参阅我的博客文章:简单多线程,第 1 部分简单多线程,第 2 部分

于 2013-07-25T23:24:54.160 回答
9

根据文档StreamWriter默认情况下不会在每次写入后自动刷新,因此它是缓冲的。

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

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

whereProcessLine声明如下:

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

由于ReadLines是惰性的并且WriteAllLines具有惰性重载,因此它将流式传输文件而不是尝试读取整个内容。

于 2013-07-25T23:23:21.153 回答
1

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

就像是

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。

于 2013-07-25T23:14:15.213 回答