10

我正在阅读一个包含 140 万行、大小为 24 MB(平均每行 17 个字符)的大型文本文件。

我使用的是 Delphi 2009,文件是 ANSI,但在读取时会转换为 Unicode,因此您可以说转换后的文本大小为 48 MB。

(编辑:我发现了一个更简单的例子......)

我将此文本加载到一个简单的 StringList 中:

  AllLines := TStringList.Create;
  AllLines.LoadFromFile(Filename);

我发现这些数据行似乎比它们的 48 MB 占用更多的内存。

事实上,它们使用 155 MB 内存。

我不介意 Delphi 使用 48 MB 甚至高达 60 MB 的内存管理开销。但是 155 MB 似乎过多。

这不是 StringList 的错。我之前尝试将这些行加载到记录结构中,并且得到了相同的结果(160 MB)。

我没有看到或理解是什么导致 Delphi 或 FastMM 内存管理器使用 3 倍于存储字符串所需的内存量。堆分配不可能那么低效,不是吗?

我已经对此进行了调试并尽可能地对其进行了研究。任何关于为什么会发生这种情况的想法,或者可能帮助我减少过度使用的想法将不胜感激。

注意:我以这个“较小”的文件为例。我真的想加载一个 320 MB 的文件,但 Delphi 要求超过 2 GB 的 RAM 并且由于这个多余的字符串要求而耗尽内存。

附录:Marco Cantu 刚刚发表了一份关于 Delphi 和 Unicode 的白皮书。Delphi 2009 将每个字符串的开销从 8 个字节增加到 12 个字节(对于指向字符串的实际指针,可能还要增加 4 个字节)。每 17x2 = 34 字节的行额外增加 16 字节几乎增加了 50%。但我看到超过 200% 的开销。额外的 150% 可能是什么?


成功!!感谢大家的建议。你们都让我思考。但我必须感谢 Jan Goyvaerts 的回答,因为他问:

...你为什么要使用 TStringList?文件真的必须作为单独的行存储在内存中吗?

这使我找到了一个解决方案,即我可以将我的行分组到我的程序知道的自然组中,而不是将 24 MB 文件作为 140 万行 StringList 加载。因此,这导致将 127,000 行加载到字符串列表中。

现在每行平均 190 个字符而不是 17 个。每个 StringList 行的开销是相同的,但现在行数要少得多。

当我将此应用到 320 MB 文件时,它不再耗尽内存,现在加载不到 1 GB 的 RAM。(而且加载只需要大约 10 秒,相当不错!)

解析分组的行会有一点额外的处理,但在每个组的实时处理中应该不明显。

(如果您想知道,这是一个家谱程序,这可能是我需要让它在不到 30 秒的时间内将大约 100 万人的所有数据加载到 32 位地址空间中的最后一步。所以我我们仍然有 20 秒的缓冲时间来将索引添加到数据中,这将需要允许显示和编辑数据。)

4

8 回答 8

10

你让我亲自在这里回答你的问题。我不知道您看到如此高内存使用率的确切原因,但您需要记住 TStringList 不仅仅是加载您的文件。这些步骤中的每一个都需要内存,这可能会导致内存碎片。TStringList 需要将您的文件加载到内存中,将其从 Ansi 转换为 Unicode,将其拆分为每一行的一个字符串,并将这些行填充到一个将被多次重新分配的数组中。

我的问题是你为什么要使用 TStringList?文件真的必须作为单独的行存储在内存中吗?您是要修改内存中的文件,还是只显示其中的一部分?将文件作为一大块保存在内存中并使用与您想要的部分匹配的正则表达式扫描整个文件将比存储单独的行更节省内存。

此外,是否必须将整个文件转换为 Unicode?虽然您的应用程序是 Unicode,但您的文件是 Ansi。我的一般建议是尽快将 Ansi 输入转换为 Unicode,因为这样做可以节省 CPU 周期。但是当您有 320 MB 的 Ansi 数据将作为 Ansi 数据保留时,内存消耗将成为瓶颈。尝试将文件作为 Ansi 保存在内存中,并且仅将您将显示给用户的部分转换为 Ansi。

如果 320 MB 文件不是您要从中提取某些信息的数据文件,而是您要修改的数据集,请考虑将其转换为关系数据库,并让数据库引擎担心如何管理庞大的数据集内存有限。

于 2008-11-23T10:39:45.053 回答
8

如果您使用 AnsiString 制作原始记录会怎样?立马把它切成两半?仅仅因为 Delphi 默认使用 UnicodeString 并不意味着您必须使用它。

此外,如果您确切知道每个字符串的长度(在一两个字符内),那么最好使用短字符串并减少更多字节。

我很好奇是否有更好的方法来完成您正在尝试做的事情。将 320 MB 的文本加载到内存中可能不是最好的解决方案,即使您可以将其降低到只需要 320 MB

于 2008-11-23T07:19:09.573 回答
6

我使用的是 Delphi 2009,文件是 ANSI,但在读取时会转换为 Unicode,因此您可以说转换后的文本大小为 48 MB。

对不起,但我完全不明白这一点。如果你需要你的程序是 Unicode,那么文件是“ANSI”(它必须有一些字符集,如 WIN1252 或 ISO8859_1)肯定不是正确的。我首先将其转换为 UTF8。如果文件不包含任何字符 >= 128 它不会改变任何东西(它甚至会是相同的大小),但你已经为未来做好了准备。

现在您可以将其加载到 UTF8 字符串中,这不会使您的内存消耗加倍。将同时在屏幕上可见的少数字符串即时转换为 Delphi Unicode 字符串会更慢,但考虑到内存占用更小,您的程序将在很少(免费)的系统上执行得更好记忆。

现在,如果您的程序仍然使用 TStringList 消耗太多内存,您始终可以在程序中使用 TStrings 甚至 IStrings,并编写一个实现 IStrings 或继承 TStrings 的类,并且不会将所有行保留在内存中。想到的一些想法:

  1. 将文件读入 TMemoryStream,并维护一个指向行首字符的指针数组。返回一个字符串很容易,你只需要在行的开头和下一行的开头之间返回一个正确的字符串,去掉 CR 和 NL。

  2. 如果这仍然消耗太多内存,请将 TMemoryStream 替换为 TFileStream,并且不要维护 char 指针数组,而是维护行开始的文件偏移量数组。

  3. 您还可以将 Windows API 函数用于内存映射文件。这允许您使用内存地址而不是文件偏移量,但不会像第一个想法那样消耗那么多内存。

于 2008-11-23T14:25:11.837 回答
4

默认情况下,Delphi 2009 的 TStringList 将文件读取为 ANSI,除非有字节顺序标记将文件标识为其他内容,或者如果您提供编码作为 LoadFromFile 的可选第二个参数。

因此,如果您看到 TStringList 占用的内存比您想象的要多,则说明发生了其他事情。

于 2008-11-23T08:29:37.000 回答
3

您是否有机会使用来自 sourceforge 的 FastMM 源代码并定义了 FullDebugMode 来编译程序?在这种情况下,FastMM 并没有真正释放未使用的内存块,这可以解释问题。

于 2008-11-23T09:21:45.990 回答
1

您是否依赖 Windows 来告诉您程序使用了多少内存?它因夸大 Delphi 应用程序使用的内存而臭名昭著。

不过,我确实在您的代码中看到了大量额外的内存使用。

您的记录结构是 20 个字节——如果每行有一个这样的记录,那么您正在查看的记录数据多于文本数据。

此外,字符串具有固有的 4 字节开销——另外 25%。

我相信在 Delphi 的堆处理中有一定数量的分配粒度,但我不记得它目前是什么。即使是 8 个字节(空闲块链表的两个指针),您也看到了另外 25%。

请注意,我们的增幅已经超过 150%。

于 2008-11-23T05:39:14.050 回答
1

其中一部分可能是块分配算法。随着列表的增长,它开始增加在每个块上分配的内存量。我已经很久没有看过它了,但我相信每次内存不足时,它都会使最后分配的数量增加一倍。当您开始处理如此大的列表时,您的分配也比您最终需要的要大得多。

编辑-正如lkessler 指出的那样,这种增长实际上只有 25%,但仍应将其视为问题的一部分。如果您刚刚超出临界点,则可能有一个巨大的内存块分配给未使用的列表。

于 2008-11-24T18:46:47.160 回答
0

为什么要将这么多数据加载到 TStringList 中?列表本身会有一些开销。也许 TTextReader 可以帮助你。

于 2008-11-24T15:48:10.793 回答