TStringList.LoadFromFile - 处理大文本文件时的异常情况

6

我正在运行 Delphi RAD Studio XE2。

我有一些非常大的文件,每个文件包含大量行。这些行很小 - 只有 3 个制表符分隔的双精度数据。我想要使用 TStringList.LoadFromFile 将文件加载到一个 TStringList 中,但是这会导致加载大文件时出现异常。

对于包含 200 万行(约 1GB)的文件,我会得到 EIntOverflow 异常。对于更大的文件(例如包含 2000 万行和约 10GB 的文件),我会得到 ERangeCheck 异常。

我有 32GB 的内存可以使用,只是想快速地加载并使用这个文件。发生了什么,我还有哪些其他选择?我能否使用带有大缓冲区的文件流将此文件加载到 TStringList 中?如果可以,请提供一个示例。


我只是想知道,为什么你要加载2000万行文本?你可以尝试使用TFileStream,或许会更好。 - Jerry Dodge
你有没有一个示例展示如何使用TFileStream将文本文件的行读入到TStringList中? - Trojanian
我更喜欢将文件的行存储在数据库的表中。这样操作会比使用T*List的后代快得多。所以问题是你打算如何处理这些数据? - iPath ツ
1
简单来说,真正的解决方案是停止尝试将整个文件加载到内存中。 - David Heffernan
1个回答

20

当Delphi在Delphi 2009中转换为Unicode时,TStrings.LoadFromStream()方法(该方法内部调用TStrings.LoadFromFile())对于大型流/文件变得非常低效。

在内部,LoadFromStream()将整个文件读入内存作为TBytes,然后使用TEncoding.GetString()将其转换为UnicodeString(该方法解码字节到TCharArray,将其复制到最终的UnicodeString,然后释放数组),然后解析UnicodeString(同时TBytes仍在内存中),根据需要将子字符串添加到列表中。

因此,在LoadFromStream()退出之前,内存中有四个文件数据的副本 - 三个副本最坏情况下占用filesize * 3字节的内存(其中每个副本都使用自己的连续内存块+一些MemoryMgr开销),另一个副本是解析后的子字符串!尽管在LoadFromStream()实际退出时会释放前三个副本。但这就解释了为什么在达到该点之前您会遇到内存错误 - LoadFromStream()试图使用3-4 GB的内存来加载1GB的文件,而RTL的内存管理器无法处理。

如果您想将大型文件的内容加载到TStringList中,最好使用TStreamReader而不是LoadFromFile()TStreamReader使用缓冲文件I/O方法以小块读取文件。只需在循环中调用其ReadLine()方法,并将每行Add()TStringList中即可。例如:

//MyStringList.LoadFromFile(filename);
Reader := TStreamReader.Create(filename, true);
try
  MyStringList.BeginUpdate;
  try
    MyStringList.Clear;
    while not Reader.EndOfStream do
      MyStringList.Add(Reader.ReadLine);
  finally
    MyStringList.EndUpdate;
  end;
finally
  Reader.Free;
end;

也许有一天,LoadFromStream() 可能会像这样重写,内部使用 TStreamReader


1
TStringList 在每次 Add() 时不会调用 ReallocMem(),它会以指数容量增长其内存。 - Remy Lebeau
1
即使你不知道列表中有/将有多少项,通过预先设置Capacity为一个具有代表性的大数值(如果可以的话,最好猜一下),然后在项目加载完成时将其设置为实际计数,可以获得巨大的性能提升以回收任何“浪费”。在这种情况下,可以根据文件中每行的格式已知(3个制表符分隔的双精度值)进行所需容量的良好估算:capacity := 文件大小 / 平均行长度 - Deltics
1
@Trojanian:是的,那就是要使用的构造函数。DetectBOM会告诉读者是否可以查看文件开头以查看是否有指定数据编码的BOM。否则,您必须在Encoding参数中指定编码。由于您正在加载文本文件,并且TStreamReader(和TStringList)操作Unicode字符串,因此读取器需要知道文件编码以便在读取时将文本解码为Unicode。 - Remy Lebeau
2
@Trojanian: Deltics告诉你如何预设容量:capacity := file size / average line length。例如:MyStringList.Capacity := Reader.BaseStream.Size div AverageLineLength; 您必须根据实际数据的外观提供AverageLineLength的值。 - Remy Lebeau
@RemyLebeau:谢谢,非常条理清晰。我从这篇文章中学到了东西。 :-) - Trojanian
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接