为什么Delphi中字符串的内存过多?

时间:2008-11-23 04:38:34

标签: delphi memory-management delphi-2009 utf-16 fastmm

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

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

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

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

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

我发现数据行似乎占用了比48 MB更多的内存。

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

我不介意使用48 MB甚至60 MB的Delphi允许一些内存管理开销。但155 MB似乎过多。

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

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

我调试了这个,并尽可能地研究它。任何关于为什么会发生这种情况的想法,或者可能有助于减少过量使用的想法都会受到高度赞赏。

注意:我使用这个“较小”的文件作为示例。我真的想加载一个320 MB的文件,但由于这个多余的字符串要求,Delphi要求超过2 GB的RAM和内存不足。

Addenum:Marco Cantu刚出来a White Paper on Delphi and 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文件时,它不再耗尽内存,现在加载的RAM不到1 GB。 (它只需要大约10秒钟加载,这非常好!)

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

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

8 个答案:

答案 0 :(得分:9)

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

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

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

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

答案 1 :(得分:8)

如果您使用AnsiString制作原始记录怎么办?那立刻把它砍成两半?仅仅因为Delphi默认使用UnicodeString并不意味着你必须使用它。

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

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

答案 2 :(得分: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函数用于内存映射文件。这允许你使用内存地址而不是文件偏移量,但不会消耗那么多内存作为第一个想法。

答案 3 :(得分:4)

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

因此,如果您看到TStringList占用的内存比您想象的多,那么还会有其他事情发生。

答案 4 :(得分:3)

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

答案 5 :(得分:1)

您是否依赖Windows来告诉您程序使用了多少内存?夸大Delphi app使用的内存是臭名昭着的。

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

你的记录结构是20个字节 - 如果每行有一个这样的记录,你会看到更多的记录数据而不是文本。

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

我相信Delphi的堆处理有一定的分配粒度,但我不记得它目前是什么。即使是8个字节(两个指针用于链接的空闲块列表),你还要看另外25%。

请注意,我们已经增加了超过150%。

答案 6 :(得分:1)

部分可能是块分配算法。随着列表的增长,它开始增加每个块分配的内存量。我很长一段时间没有看过它,但我相信每次耗尽内存时最后分配的数量就会翻倍。当您开始处理大型列表时,您的分配也比您最终需要的大得多。

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

答案 7 :(得分:0)

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