如何解决内存分段并强制FastMM将内存释放给操作系统?

6
注意:32位应用程序,不打算迁移到64位。
我正在使用一个非常消耗内存的应用程序,并且已经在内存分配/释放方面优化了所有相关路径。(据我所知和测试,应用程序本身没有任何内存泄漏、句柄泄漏或其他类型的泄漏。当然,我无法操作的第三方库是可能的,但在我的情况下不太可能出现这种情况)
该应用程序将经常分配大型单维和双维动态数组,其中包含多达4个浮点数的单个和压缩记录。通过大型我指的是5000x5000的记录(单,单,单,单)是正常的。同时在工作中甚至有6到7个这样的数组。这是必要的,因为在这些数组上进行了许多交叉计算,如果从磁盘读取它们会导致性能严重下降。
在澄清了这一点之后,由于这些大型动态数组不会在释放后消失,即使将它们设置为0或完成它们,我经常会遇到内存不足错误。我知道这当然是FastMM为了速度而做的事情。
我使用以下方法跟踪FastMM分配的块和进程消耗的内存(RAM + PF):
function CurrentProcessMemory(AWaitForConsistentRead:boolean): Cardinal;
var
  MemCounters: TProcessMemoryCounters;
  LastRead:Cardinal;
  maxCnt:integer;
begin
  result := 0;// stupid D2010 compiler warning
  maxCnt := 0;
  repeat
    Inc(maxCnt);
    // this is a stabilization loop;
    // in tight loops, the system doesn't get
    // much chance to release allocated resources, which in turn will get falsely
    // reported by this function as still being used, resulting in a false-positive
    // memory leak report in the application.
    // so we do a tight loop here, waiting, until the application reported memory
    // gets stable.
    LastRead := result;
    MemCounters.cb := SizeOf(MemCounters);
    if GetProcessMemoryInfo(GetCurrentProcess,
        @MemCounters,
        SizeOf(MemCounters)) then
      Result := MemCounters.WorkingSetSize + MemCounters.PagefileUsage
    else
      RaiseLastOSError;
    if AWaitForConsistentRead and (LastRead <> 0) and (abs(LastRead - result)>1024) then
    begin
      sleep(60);
      application.processmessages;
    end;
  until (not AWaitForConsistentRead) or (abs(LastRead - result)<1024) or (maxCnt>1000);
  // 60 seconds wait is a bit too much
  // so if the system is that "unstable", let's just forget it.
end;

function CurrentFastMMMemory:Cardinal;
var mem:TMemoryManagerUsageSummary;
begin
  GetMemoryManagerUsageSummary(mem);
  result := mem.AllocatedBytes + mem.OverheadBytes;
end;

我在一台64位电脑上运行代码,崩溃前的最高内存消耗约为3.3-3.4 GB。之后,在应用程序的任何位置都会出现与内存/资源相关的崩溃。我花了一些时间才将其锁定在大型动态数组使用上,这些数组被深深地嵌入到某些第三方库中。
我采取的解决方法是通过重新启动应用程序并关闭带有特定参数的应用程序来使应用程序从上次离开的地方恢复自身。如果内存消耗合理且当前操作完成,则这很好。但当当前内存使用量为1GB,而要处理的下一个操作需要2.5GB或更多的内存时,就会出现大问题。我的当前代码将自身限制在使用1.5GB内存之前进行恢复,但在这种情况下,我必须将限制降低到1GB以下,这基本上意味着应用程序在每个操作之后都会恢复自身,即使如此也不能保证一切都会顺利。
如果另一个操作需要处理更大的数据集,并且需要总共4GB或更多的内存,怎么办?
请注意,我所说的不是实际内存中的4GB,而是通过分配巨大的动态数组消耗的内存,操作系统在解除分配后无法获取回来,因此仍然将其视为已使用的内存,所以会增加内存消耗。
因此,我的下一个攻击点是强制FastMM释放所有(或至少部分)内存给操作系统。我特别针对巨大的动态数组进行操作。同样,这些数组在第三方库中,因此重新编码不是最佳选择。在FastMM代码中进行微调并编写过程以释放内存要容易得多且更快速。
我不能从FastMM切换,因为当前整个应用程序和一些第三方库都是围绕使用PushAllocationGroup编写的,以便快速查找和定位任何内存泄漏。我知道我可以编写一个虚拟的FastMM单元来解决编译引用问题,但我将没有这种快速而确定的泄漏检测功能。
总之:有没有办法强制FastMM释放其至少一部分大块到操作系统?(好吧,当然有,真正的问题是:是否有人写过它,如果是,请分享?)
谢谢
稍后编辑:
我很快就会提供一个小的相关测试应用程序。看起来模拟一个应用程序并不容易。

2
很有趣,你在问题中提到了内存分段,但没有提到内存碎片化的问题。 - Marcus Adams
你为什么要打包记录?这通常会导致性能更差,因为会出现错位。 - David Heffernan
关于“愚蠢的D2010编译器警告”,编译器是准确的。如果你移除 result := 0 那么 LastRead := result 就会读取一个未初始化的变量。 - David Heffernan
@David Heffernan:当发表评论时,初始代码没有循环。试一下。RaiseLastOSError调用未被视为异常,因此它被认为是一条有效路径,并且该函数被警告/提示为未返回值(Delphi 2010)。我认为这也会在调用引发中止的过程时发生。 - ciuly
@David Heffernan:在这个应用程序中,它们的大小从100多兆字节到800多兆字节不等。我会更新问题的描述。 - ciuly
显示剩余6条评论
3个回答

4
我怀疑问题实际上并不是由FastMM引起的。对于巨大的内存块,FastMM不会进行任何子分配。您的分配请求将使用直接的VirtualAlloc处理。然后,解除分配是通过VirtualFree完成的。
这是假设您正在一个连续的块中分配那些380MB的对象。我怀疑您实际上拥有的是不规则的2D动态数组。它们不是单个分配。一个5000x5000的不规则2D动态数组需要5001次初始化分配。一次是行指针,另外5000次是行。这些将是中等大小的FastMM块。将会有子分配。
我认为您要求太多了。根据我的经验,在32位进程中,任何时候当您需要超过3GB的内存时,都无法实现。地址空间的碎片化将在您用尽内存之前使您停止。您不能指望这能够工作。切换到64位,或使用更聪明、要求较少的分配模式。或者您真的需要密集的2D数组吗?您可以使用稀疏存储吗?
如果您无法通过这种方式减轻内存需求,您可以使用内存映射文件。这将允许您利用64位系统具有的额外内存。系统的磁盘缓存可以大于4GB,因此您的应用程序可以遍历超过4GB的内存而不实际需要访问磁盘。
您可以尝试不同的内存管理器。我真的不抱任何希望它会有所帮助。您可以编写一个使用HeapAlloc的微不足道的替代内存管理器。并启用低碎片堆(从Vista默认启用)。但我真的怀疑它会有所帮助。恐怕对于您来说,没有快速解决方案。要解决这个问题,您需要对代码进行更根本的修改。

@FreeConsulting 如果代码中指定了编译器为D2010,那么就不支持64位编译器。或者我错过了问题中提到使用64位编译器的部分,请指出来给我看看。 - David Heffernan
我没有切换到64位,也没有计划这样做。问题不在于需要3GB,而在于需要10次500MB。你分配了一个:数组的数组的记录a、b、c、d:single; end;并且执行SetLength(a, 5000, 5000); 做任何事情SetLength(a, 0, 0)或Finalize(a);并且你在循环中这样做,看看会发生什么。几次迭代后,内存用尽了。称其为分段、碎片化,甚至是“泄漏”:在未来,这种情况不应该发生。 - ciuly
这里完全没有问题。我可以轻松进行1000次迭代。我认为我可以一直做下去。我相信你的实际程序会做更多事情。你真的不能期望在4GB程序中保留3GB地址空间,并且有很多分配/重新分配。那必然会导致碎片化。 - David Heffernan
1
啊,我忽略了那个关于愚蠢的评论,抱歉。 - Free Consulting
@David Heffernan:是的,我的程序还有更多功能。我认为这已经足够作为一个测试案例(基于我对应用程序进行的调试)。但我现在发现它并不足够。我明天会制作一个可重现的测试应用程序(现在已经快1点了)。 - ciuly

2
您的问题,正如其他人所说,很可能是由于内存碎片化引起的。您可以使用VirtualQuery来创建一个关于内存分配给您的应用程序的图片来测试这一点。您很可能会发现,尽管您可能有足够的总内存来创建新的数组,但您没有足够连续的内存空间。
FastMem已经尽力避免由于内存碎片化而导致的问题。 "小"分配是在地址空间的低端完成的,而"大"分配是在高端完成的。这可以避免一个常见的问题:一系列的大型和小型分配,然后释放所有大型分配导致大量的碎片化内存,几乎无法使用。(对于比原始大型分配稍大的任何内容都无法使用。)
为了看到FastMem方法的好处,想象一下您的内存布局如下:
每个数字代表100mb的块。
[0123456789012345678901234567890123456789] 小型分配由“s”表示。
大型分配由大写字母表示。
[0sssss678901GGGGFFFFEEEEDDDDCCCCBBBBAAAA] 现在,如果您释放所有大块,以后执行类似的大型分配应该没有问题。
[0sssss6789012345678901234567890123456789] 问题在于,“大”和“小”是相对的,并且高度依赖于您的应用程序的性质。FastMem定义了“大”和“小”之间的分界线。如果您恰好有一些FastMem将其分类为大的小型分配,则可能会遇到以下问题。 [0sss4sGGGGsFFFFsEEEEsDDDDsCCCCsBBBBsAAAA] 现在,如果您释放大块,则剩下:
[0sss4s6789s1234s6789s1234s6789s1234s6789] 尝试分配大于400mb的东西将失败。

选项

您可以调整FastMem设置,使所有“小”分配也被FastMem视为小。然而,有一些情况下这种方法不起作用:
- 任何使用绕过FastMem分配内存给应用程序的DLL可能仍会导致碎片化。 - 如果不释放所有大块,则剩余的大块可能会导致碎片化,随着时间的推移逐渐恶化。
您可以自己承担内存管理的任务:
- 分配一个非常大的块,例如3.5GB,该块将在应用程序的整个生命周期中保留。 - 不使用动态数组,而是确定设置新数组时要使用的指针位置。
当然,最简单的替代方案是使用64位。
您可以考虑备选数据结构:
- 您是否真的需要数组查找功能?如果不需要,另一种分配较小块的结构可能足够。 - 即使您需要数组查找,也可以考虑分页数组。稀疏数组是数组和链表的组合。数据存储在页面上,每个页面链接到链表。 - 一种简单的变体(因为您提到您的数组是二维的)是利用它:一个维度形成自己的数组,为第二维的多个数组之一提供查找。
与备选数据结构选项相关的是,考虑将一些数据存储在磁盘上。是的,性能会变慢。但是,如果可以找到有效的缓存机制,那么可能不会太慢。最好稍微慢一点,而不是崩溃。

0

在Delphi中,动态数组是引用计数的,因此当它们不再使用时应该自动释放。与字符串一样,当在多个变量/对象中共享/存储时,它们使用COW(写时复制)进行处理。因此,似乎您有某种内存/引用泄漏(例如,内存中的一个对象仍然保持对数组的引用)。

只是为了确保:您没有做任何低级指针技巧,对吗?

所以,请发布一个测试程序(或通过电子邮件私下发送完整程序),以便我们中的一位可以查看它。


1
释放内存给内存管理器并不意味着它会被释放到系统中。子分配内存管理器可能会保留内存并重新使用它。另外,请不要建议私人电子邮件。这个网站是关于分享的。 - David Heffernan

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