【问题标题】:TStringList of objects taking up tons of memory in Delphi XE在 Delphi XE 中占用大量内存的对象的 TStringList
【发布时间】:2011-11-03 20:21:57
【问题描述】:

我正在开发一个模拟程序。

程序做的第一件事就是读入一个巨大的文件(28 mb,大约 79000 行),解析每一行(大约 150 个字段),为对象创建一个类,并将其添加到字符串列表。

它还读取另一个文件,该文件在运行期间添加了更多对象。最后,它最终变成了大约 85,000 个对象。

我使用的是 Delphi 2007,该程序使用了大量内存,但运行正常。我升级到 Delphi XE,并将程序迁移过来,现在它使用了更多的内存,最终在运行中途内存不足。

所以在 Delphi 2007 中,读取初始文件后最终会使用 1.4 gigs,这显然是一个巨大的数量,但在 XE 中,它最终会使用几乎 1.8 gigs,这确实是巨大的并导致用完并得到错误

所以我的问题是

  1. 为什么要使用这么多内存?
  2. 为什么它在 XE 中使用的内存比 2007 多得多?
  3. 对此我能做些什么?我无法更改文件的大小或长度,我确实需要为每一行创建一个对象并将其存储在某处

谢谢

【问题讨论】:

  • 你确定数字不是 1.4 和 2.8?
  • Unicode 比 AnsiString 大。这是基于 TStringList 类的。
  • 在您可以完全依赖 64 位进程之前,您应该重新设计您的应用程序以更节俭地使用内存。即使是 1.4gb,您也会在 32 位系统上突破地址空间的限制。
  • @Andreas:如果他所有的数据都是字符串,是的,大约是 2.8GB,但我假设字符串数据是 800MB(与 Ansi 中的 400MB 相比),其余的(1GB)被他的物品占据。正如 David Heffernan 所说,在其中一个 cmets 中,1.4GB 和 1.8GB 都在突破极限。
  • @David:好吧,那么我想我们真的在推动应用程序加载后内存中可用的 90+ GB 模型(200+ 百万个实例)的极限。 6 小时...(并且仍然为 100 名用户提供服务)您算算,这是不可能的,但我们做到了。不要问我怎么做,因为...我只知道广义上的。有几个 wiz 同事了解详细信息 :-)) 但我们肯定没有使用 TStringLists 或 TObjectLists 作为容器......哎呀,我们的对象中甚至没有数据,我们忽略/覆盖了监视器字段在 D2009 中偷偷引入。 64位转换会很有趣:-))

标签: delphi memory-management tstringlist


【解决方案1】:
  • 在Delphi 2007(及更早版本)中,字符串是Ansi字符串,即每个字符占用1个字节的内存。

  • 在Delphi 2009(及更高版本)中,字符串是Unicode字符串,即每个字符占用2个字节的内存。

AFAIK,没有办法让 Delphi 2009+ TStringList 对象使用 Ansi 字符串。您真的在使用TStringList 的任何功能吗?如果没有,您可以使用字符串数组。

那么,你自然可以选择

type
  TAnsiStringArray = array of AnsiString;
  // or
  TUnicodeStringArray = array of string; // In Delphi 2009+, 
                                         // string = UnicodeString

【讨论】:

  • 我真正想做的唯一一件事就是存储对象,然后有办法删除/添加它们。我还使用 FindIdx 搜索列表以查找要更新/删除的对象
  • 字符串数组会使用更少的内存吗? ISTM 认为他的对象消耗了大部分内存。我不知道这些对象是什么,但他可能应该找到一种更好的方法来处理和存储它们,例如内存映射文件。但这可能需要大量重写。
  • 问题可能出在对象上,它们有大约 60 个字段,其中大约 10 个是列表。大多数列表都很​​小,但特别是一个有 200 个字段长
  • @Rudy,是的,内存映射文件也是我推荐的技术之一,尽管它需要重写。
  • @KingOfKong:看看你能做些什么来处理重复。对字符串处理知之甚少,无法知道当您不断添加恰好具有相同字符串内容的新对象时,字符串引用计数是否会有所帮助,但您可以尝试通过识别冗余/重复数据和重用实例化对象来减少内存消耗更早:即当一个实例与另一个实例具有相同的列表内容时。
【解决方案2】:

当您在没有看到代码和类声明的情况下将其解析为对象时,很难说为什么您的 28 MB 文件会扩展到 1.4 GB 的对象。另外,您说您将其存储在TStringList 而不是TListTObjecList。这听起来像是您将它用作某种字符串-> 对象键/值映射。如果是这样,您可能需要查看 XE 中 Generics.Collections 单元中的 TDictionary 类。

至于为什么在 XE 中使用更多内存,是因为 string 类型在 Delphi 2009 中从 ANSI 字符串更改为 UTF-16 字符串。如果您不需要 Unicode,可以使用 TDictionary节省空间。

此外,为了节省更多内存,如果您不需要立即使用全部 79,000 个对象,还可以使用另一个技巧:延迟加载。这个想法是这样的:

  • 将文件读入 TStringList。 (这将使用与文件大小一样多的内存。如果将其转换为 Unicode 字符串,可能会使用两倍。)不要创建任何数据对象。
  • 当您需要特定的数据对象时,调用一个检查字符串列表并查找该对象的字符串键的例程。
  • 检查该字符串是否有与之关联的对象。如果不是,则从字符串创建对象并将其与 TStringList 中的字符串相关联。
  • 返回与字符串关联的对象。

这将减少您的内存使用和加载时间,但只有在加载后您不需要所有(或大部分)对象时才有用。

【讨论】:

  • 感谢您的回复。不幸的是,我确实需要所有的对象。该程序是一个器官分配器。我读入了最初的患者候补名单,然后我读入了一个新患者,更新了名单上的患者,或者尝试分配器官,当我分配时,我需要查看所有患者是否匹配(这需要拥有所有患者值)
  • @KingOfKong:如果您有特定的标准要尝试与之匹配,我建议您更改架构。您所描述的是关系数据库和 SQL 查询的完美匹配。这样做会大大减少您的内存使用量和运行搜索所需的时间。
  • +1 除非要求真的是非常时间紧迫的,否则在搜索过程中动态重新分配一个对象应该可以正常工作。它基本上是内存使用和持续时间之间的选择。
  • 请记住,TStringlist 需要两倍的内存量才能加载(在 D2007 中也是如此)。一次加载,一次拆分成字符串。前者被释放,后者被保留。
【解决方案3】:

除了安德烈亚斯的帖子:

在 Delphi 2009 之前,一个字符串头占用 8 个字节。从 Delphi 2009 开始,字符串标头占用 12 个字节。所以每个唯一字符串比以前多使用 4 个字节,+ 每个字符占用两倍内存的事实。

此外,我相信从 Delphi 2010 开始,TObject 开始使用 8 个字节而不是 4 个字节。因此,对于由 delphi 创建的每个单个对象,delphi 现在多使用 4 个字节。添加这 4 个字节是为了支持我相信的 TMonitor 类。

如果您迫切需要节省内存,如果您有大量重复的字符串值,这里有一个小技巧可能会有所帮助。

var
  uUniqueStrings : TStringList;

function ReduceStringMemory(const S : String) : string;
var idx : Integer;
begin
  if not uUniqueStrings.Find(S, idx) then
    idx := uUniqueStrings.Add(S);

  Result := uUniqueStrings[idx]
end;

请注意,只有当您有大量重复的字符串值时,这才会有所帮助。例如,这段代码在我的系统上使用少了 150mb。

var sl : TStringList;
  I: Integer;
begin
  sl := TStringList.Create;
  try
    for I := 0 to 5000000 do
      sl.Add(ReduceStringMemory(StringOfChar('A',5)));every
  finally
    sl.Free;
  end;
end;

【讨论】:

  • 我刚刚检查过:TObject.Create.InstanceSize=4 在 Delphi 2010 之前 - 这只是指向类类型的指针。
  • @Rudy 没错。对于仍然无法正常工作的 Monitor 字段来说就这么多。拖钓结束。 ;)
  • 关于内存消耗,比这要复杂一点。根据内存大小,它有一个标题和一些对齐。这取决于所使用的内存管理器,但由于每个块的分配结构和重新分配准备,一个普通对象将使用超过 4 个字节的内存堆,无论如何(每个内存块至少有 4 个指针头)。从全局内存消耗 POV 来看,从 4 字节更改为 8 字节并没有改变任何东西(至少在使用 FastMM4 时)。
【解决方案4】:

我还在我的程序中读取了很多字符串,这些字符串对于大文件可能接近几 GB。

无需等待 64 位 XE2,这里有一个可能对您有所帮助的想法:

我发现将单个字符串存储在字符串列表中既慢又浪费内存。我最终把琴弦挡在了一起。我的输入文件有逻辑记录,可能包含 5 到 100 行。因此,我没有将每一行存储在字符串列表中,而是存储每条记录。处理一条记录以查找我需要的行会为我的处理增加很少的时间,所以这对我来说是可能的。

如果您没有逻辑记录,您可能只想选择一个分块大小,并将每(例如)10 个或 100 个字符串存储为一个字符串(用分隔符分隔它们)。

另一种选择是将它们存储在一个快速有效的磁盘文件中。我推荐的是Arnaud Bouchez 的开源Synopse Big Table

【讨论】:

  • 感谢您对开源项目的宣传!
【解决方案5】:

虽然阅读 cmets,但听起来您需要将数据从 Delphi 中取出并放入数据库中。

从那里很容易将器官捐赠者与接受者匹配*)

SELECT pw.* FROM patients_waiting pw
INNER JOIN organs_available oa ON (pw.bloodtype = oa.bloodtype) 
                              AND (pw.tissuetype = oa.tissuetype)
                              AND (pw.organ_needed = oa.organ_offered)
WHERE oa.id = '15484'

如果您想查看可能与新器官捐赠者 15484 匹配的患者。

在记忆中,您只处理少数匹配的患者。

*) 简化得面目全非,但仍然如此。

【讨论】:

  • 是的 - 当您必须处理复杂的查询时,SQL 选项总是很好。如果您创建适当的索引,SQL 规划器可能会比在内存中强力查找对象做更好的优化工作。我怀疑你甚至可以为此目的使用内存数据库,例如使用 SQLite。
【解决方案6】:

只有一个可以节省内存的想法。

您可以让数据保留在原始文件中,然后从内存结构中指向它们。

例如,这就是我们为browsing big log files almost instantly 所做的事情:我们对日志文件内容进行内存映射,然后快速解析它以在内存中创建有用信息的索引,然后我们动态地读取内容。读取期间不创建字符串。只有指向每一行开头的指针,动态数组包含所需的索引。调用TStringList.LoadFromFile 肯定会慢得多并且消耗内存。

代码是here - 请参阅TSynLogFile 类。诀窍是只读取一次文件,并即时创建所有索引。

例如,下面是我们如何从 UTF-8 文件内容中检索一行文本:

function TMemoryMapText.GetString(aIndex: integer): string;
begin
  if (self=nil) or (cardinal(aIndex)>=cardinal(fCount)) then
    result := '' else
    result := UTF8DecodeToString(fLines[aIndex],GetLineSize(fLines[aIndex],fMapEnd));
end;

我们对parse JSON content 使用完全相同的技巧。使用这种混合方式使用by the fastest XML access libraries

为了处理您的高级数据并快速查询它们,您可以尝试使用动态记录数组,以及我们优化的TDynArrayTDynArrayHashed 包装器(在同一单元中)。记录数组消耗的内存更少,搜索速度更快,因为数据不会被碎片化(如果使用有序索引或散列会更快),并且您将能够对内容进行高级访问(例如,您可以定义自定义函数以从内存映射文件中检索数据)。动态数组不适合快速删除项目(或者您必须使用查找表) - 但您写道您并没有删除太多数据,因此在您的情况下这不会成为问题。

所以你不会再有任何重复的结构,只有 RAM 中的逻辑和内存映射文件上的数据 - 我在这里添加了一个“s”,因为相同的逻辑可以完美地映射到多个源数据文件(您需要一些“合并”和“实时刷新”AFAIK)。

【讨论】:

    【解决方案7】:

    我建议您尝试使用绝地类库 (JCL) 类 TAnsiStringList,它类似于 Delphi 2007 中的 TStringList,因为它是由 AnsiStrings 组成的。

    即便如此,正如其他人所提到的,XE 将使用比 delphi 2007 更多的内存。

    我真的看不出将巨大的平面文件的全文加载到字符串列表中的价值。其他人提出了一种大表方法,例如 Arnaud Bouchez 的方法,或者使用 SqLite 或类似的方法,我同意他们的看法。

    我认为您还可以编写一个简单的类,将您拥有的整个文件加载到内存中,并提供一种将逐行对象链接添加到巨大的内存中 ansichar 缓冲区的方法。

    【讨论】:

      【解决方案8】:

      从 Delphi 2009 开始,不仅字符串而且每个 TObject 的大小都增加了一倍。 (见Why Has the Size of TObject Doubled In Delphi 2009?)。但是,如果只有 85,000 个物体,这并不能解释这种增加。只有当这些对象包含许多嵌套对象时,它们的大小才可能是内存使用的相关部分。

      【讨论】:

      • 它们有,它们是: 问题可能出在对象上,它们有大约 60 个字段,其中大约 10 个是列表。大多数列表都很​​小,但特别是一个有 200 个字段长
      【解决方案9】:

      您的列表中有很多重复的字符串吗?也许尝试只存储唯一的字符串将有助于减少内存大小。看我的问题 about a string pool 提供一个可能(但可能太简单)的答案。

      【讨论】:

        【解决方案10】:

        你确定你没有遇到内存碎片的情况吗?

        请务必使用最新的FastMM(当前为4.97),然后查看UsageTrackerDemo 演示,其中包含显示Delphi 内存实际使用情况的内存映射表。

        最后看看VMMap,它向你展示了你的进程内存是如何使用的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-06-20
          • 2011-04-01
          相关资源
          最近更新 更多