Delphi:使用类存储数据 vs 记录,减少内存使用

4

我有很多数据需要在应用程序运行时存储、读取和修改。这些数据可以比作一棵树,每个节点由有限数量的字符串和整数描述,并且有很多子元素。 目前,这些数据是使用类/对象来存储的,例如:

TRootElement = class
  fName, fDescription: string;
  fPos: integer;
  /// etc
end;

fDocs: TObjectList; //list of TVariable = class(TRootElement)
fClasses: TObjectList; // list of TClass=class(TRootElement)

目前程序消耗的内存是无法接受的,因此我正在寻找限制它的解决方案。

我的问题是:如果我用基于记录的架构替换当前的OOP和基于对象的架构,内存消耗会显著降低吗?例如,通用记录可以包含以下内容:

TRootElement = record
  fType: TElemType; // enum: root, variable, class, etc ... 
  fName, fDesc: string; 
  // all the fields used by root elem and it's descendants there
end;

我应该将TList替换为指向下一个/上一个元素的指针吗?由于我从未通过索引访问列表中的元素,而是始终循环遍历整个列表,所以这不应该太难... 但是如果没有必要,我想避免这样做。

谢谢! m.


哪个版本的Delphi可以提供帮助。 - Kornel Kisielewicz
如果您向我们展示您 1.8 兆字节的基础文本文件中的一部分数据,那么或许我们可以给出更好的答案。 - Jeroen Wiert Pluimers
阅读下来,这个应用程序似乎包含“代码完成”解析树。我建议将这些内容存储在内存映射文件中,可以快速访问(加载)和丢弃,而无需重新解析就能释放内存。我认为一个存储在磁盘上的B+树文件足够快,可以完全替代你的基于RAM的系统。 - Warren P
你不会真正节省很多内存。然而,在对象的构造函数中执行一些操作是有必要的。因此,从 CPU 的角度来看,如果使用记录而不是对象,你可能会节省一些 CPU 周期(但不多)。 - Gabriel
4个回答

15
将类转换为记录将减少内存使用,但随着类或记录中字段数量的增加,节省的意义会降低。类和对应记录之间的大小差异正好为四个字节,这解释了类所持有但记录所缺少的VMT指针。在权衡考虑时,这种差异通常是微不足道的:为了节省四个字节,您放弃了继承、多态、数据隐藏和其他面向对象的特性。(Delphi的新“带方法的记录”可以缓解其中一些问题,但如果您只有Delphi 2005,则尚未拥有该功能。)
事实上,如果那四个字节真的对您的程序有影响,那么您可能有一个更大的问题要解决。只需添加另一个节点到树中,就可以消除这四个字节的节省。对于足够大的数据集,无论您使任何一个节点多么小都没有关系,因为您无法将它们全部保留在内存中。您需要调查某种缓存方案,以便仅将一些节点保留在内存中,而其余节点则保留在其他地方,例如文件或数据库中。
如果用节点的双向链表替换当前的列表,您可能会看到内存使用量增加,因为现在每个节点都要跟踪其相邻的下一个和上一个节点,而以前是TObjectList管理所有这些。

我不明白为什么TObjectList的实例比n_records * 2 * sizeof(pointer)还要小...难道TObjectList从其父类继承的所有字段都不算吗? - migajek
3
TObject占用4个字节。TList增加了12个字节,而TObjectList再增加4个字节,总的空TObjectList大小为20个字节。在内部,TList使用一个指向其元素的指针数组(每个项目额外4个字节),因此总内存使用的公式为y = 4x + 20。使用双向链表时,列表对象中每个节点都要使用两个指针(8个字节),因此公式为y = 8x。虽然它开始时更低,但斜率是TList版本的两倍,所以增长速度是两倍的。交点,即打破平衡的点,只有5 - Rob Kennedy
考虑到 FasMM 的工作方式(最小分配单元),我甚至不确定你是否真的可以节省那 4 个字节 :) - Gabriel

2
目前程序消耗的内存是不可接受的。"不可接受"的意思是什么?您对其进行了测量吗?有哪些事实(对象数量,对象大小,已使用的内存)?
如果您的程序存在内存泄漏,请使用FastMM进行检查。如果没有,请首先进行此操作。
如果您的列表经常增长,则可能存在内存碎片问题。使用列表的容量属性(如果可能)。在这种情况下,链表可以帮助解决问题,但链表需要比TList更多的内存(如果合理使用容量)。请参见如何监视或可视化Delphi应用程序的内存碎片以获取更多信息。
对于Delphi <= 2005,将Borland Memory Manager替换为FastMM可能会有所帮助。
至少,像Rob一样,我认为更改记录不能解决您的问题。

该应用程序是Web IDE,模块是PHP代码完成。不可接受的意味着启用该模块并为PHP5加载数据(带有描述(文档)的函数、类、常量)会增加10 Mb的内存使用量。现在想象一下,如果用户决定加载一些框架定义,会发生什么。我正在使用FastMM的fulldebugmode,绝对没有泄漏。 - migajek
如果数据相对静态,一个常见的CGI技巧是将其放入一个服务中,在共享内存上共享它,并通过某些IPC手段将变异返回到服务。 - Marco van de Voort
在一台拥有2GB内存的计算机中,10兆字节根本不算什么。 - Warren P
10兆字节不是问题,但这仅适用于php5。如果用户想要框架或自己的代码完成,使用量将大幅增加。 - migajek

2
与绝大多数集成开发环境相比,仅需增加10兆字节的内存来加载所有PHP5元数据实际上非常不错。如果你觉得值得一试,我会建议你从字符串文本合并开始尝试。将所有字符串放入全局表(或字典)中,并从所有字符串指向该表。你可以进一步采取这一步骤,因为PHP 5语言和库非常静态:将整个数据结构从动态转换为静态常量(使用记录),以及所有索引枚举类型。你可能能做的是将所有字符串资源或字符串常量,然后查看Delphi编译器是否可以为您执行文字合并。我刚注意到你还要加载所有PHP5内容的文档,这占用了相当多的内存。您可能希望将它们加载到压缩流中。

这些数据被频繁地检索,每次代码完成执行都需要访问,因此访问时间也非常重要。然而,与仅约1.8 Mb的纯文本文件大小(包含类/函数名称和描述)相比,其内存表示太大了。 参数: 无 - migajek

1

如果像Kornel所说可以对字符串设置限制,那就真的很重要。Ansistring有一些内部开销,还有额外的开销。然而,即使未使用,shortstring也总是被分配了。

如果内存真的很紧张,为字符串进行自己的分配更明智,特别是如果数据相对不变。然后只需分配一个大块,并将所有字符串放在其中,前缀为16位长度左右。

较少的低级技巧,例如仅对(某些)字符串进行去重可以节省大量存储空间。

请注意,Rob关于记录与类讨论仅适用于您能够静态实例化类并以非常便宜的方式分配内存的情况,这可能不是您所做的。这是因为您可以使用记录数组。否则,它始终是引用类型,会导致堆开销和-松散(快速mm,16字节粒度)

我建议不要使用tstringlist/tlist/tobjectlist,因为在非常大的列表(数百万)中插入删除可能会很痛苦,因为删除/插入是O(n),在中间插入意味着移动一半的数据。这在20-100k和1M元素之间变得痛苦,具体取决于您的访问模式如何。

使用tlist的tlists,并且不让每个tlist变得太大已经是一个很好的解决方法。

当我这样做时(为OLAP集群,当2GB服务器内存仍然是2000美元时),我曾经甚至使用指针中的对齐位来存储分配的大小类。虽然我不建议这样做 :-)

当然,使用FPC进行64位处理也是一种选择。我在不到一个小时内就将上述32位解决方案的核心服务器部分工作在64位上。


好的,我管理的内存并不是那么大。与我实际存储的相比,这是完全不能接受的。 - migajek
我在评论中看到了。只需将其记录下来,并按照上面的帖子手动分配字符串。 - Marco van de Voort

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