6

注意:32 位应用程序,不打算迁移到 64 位。

我正在使用一个非常消耗内存的应用程序,并且在内存分配/取消分配方面已经优化了所有相关路径。(没有内存泄漏,没有句柄泄漏,应用程序本身没有任何其他类型的泄漏 AFAIK 并经过测试。我无法触及的第 3 方库当然是候选库,但在我的场景中不太可能)

该应用程序将经常分配包含最多 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。之后,我在应用程序的任何地方都会遇到与内存/资源相关的崩溃。我花了一些时间来确定隐藏在某些 3rd 方库中的大型动态数组的使用情况。

我克服这个问题的方法是,我通过重新启动自身并使用某些参数关闭,使应用程序从它停止的地方恢复自身。如果内存消耗公平且当前操作完成,这一切都很好。

当当前内存使用量为 1GB 并且下一个要处理的操作需要 2.5 GB 或更多内存来处理时,就会出现大问题。我当前的代码在恢复之前将自身限制为 1.5 GB 已用内存的上限,但在这种情况下,我必须将限制降低到 1 GB 以下,这基本上会使应用程序在每次操作后自行恢复,甚至不能保证一切都会好起来的。

如果另一个操作需要处理更大的数据集并且总共需要 4GB 或更多内存怎么办?

需要注意的是,我不是在谈论实际的 4 GB 内存,而是通过分配巨大的动态数组来消耗内存,一旦取消分配,操作系统就不会返回,因此它仍然认为它已被消耗,所以它加起来。

因此,我的下一个攻击点是强制 fastmm 将所有(或至少部分)内存释放到操作系统。我专门针对这里的巨大动态数组。同样,这些都在第 3 方库中,因此重新编码并不是真正的首选。修改 fastmm 代码并编写 proc 来释放内存要容易和快捷得多。

我无法从 FastMM 切换,因为目前整个应用程序和一些第 3 方库都围绕 PushAllocationGroup 的使用进行了大量编码,以便快速查找和查明任何内存泄漏。我知道我可以编写一个虚拟的 FastMM 单元来解决编译引用问题,但是我将无法进行这种快速且确定的泄漏检测。

总之:有什么办法可以强制 FastMM 至少向操作系统释放一些大块?(嗯,当然有,实际的问题是:有没有人写过它,如果有,请分享一下?)

谢谢

稍后编辑:

我很快就会提出一个小的相关测试应用程序。模拟一个似乎并不容易

4

3 回答 3

4

我怀疑这个问题实际上归结为 FastMM。对于巨大的内存块,FastMM 不会做任何子分配。您的分配请求将直接处理VirtualAlloc。然后释放是VirtualFree.

这是假设您将这些 380MB 的对象分配到一个连续的块中。我怀疑您实际上拥有的是参差不齐的二维动态数组。而且它们不是单一的分配。一个 5000x5000 参差不齐的 2D 动态数组需要 5001 次分配来初始化。一个用于行指针,5000 个用于行。这些将是中等 FastMM 块。会有分拨。

我觉得你问得太多了。根据我的经验,任何时候你在 32 位进程中需要超过 3GB 的内存,游戏就结束了。地址空间的碎片会在内存不足之前阻止您。你不能指望这能奏效。切换到 64 位,或者使用更聪明、要求更低的分配模式。或者你真的需要密集的二维数组吗?可以使用稀疏存储吗?

如果您不能以这种方式减轻内存需求,则可以使用内存映射文件。这将允许您利用 64 位系统拥有的额外内存。系统的磁盘缓存可以大于 4GB,因此您的应用程序可以遍历超过 4GB 的内存,而无需实际访问磁盘。

您当然可以尝试不同的内存管理器。老实说,我不希望它会有所帮助。您可以编写一个简单的替换内存管理器,它使用HeapAlloc. 并启用低碎片堆(从 Vista 上默认启用)。但我真诚地怀疑它是否会有所帮助。恐怕不会有一个快速解决您的问题。要解决这个问题,您需要对代码进行更根本的修改。

于 2013-12-18T21:18:14.273 回答
2

正如其他人所说,您的问题很可能归因于内存碎片。您可以通过使用VirtualQuery来测试这一点,以创建内存分配给应用程序的图片。您很可能会发现,尽管您可能有足够多的总内存用于新数组,但您没有足够的连续内存。

FastMem 已经做了很多尝试和避免由于内存碎片引起的问题。“小”分配在地址空间的低端完成,而“大”分配在高端完成。这避免了一个常见问题,即一系列先大后小的分配,然后是所有大的分配被释放,导致大量碎片内存几乎不可用。(当然,任何比原始大分配稍大的东西都无法使用。)

要查看 FastMem 方法的好处,请想象您的内存布局如下:

每个数字代表一个 100mb 的块。
[0123456789012345678901234567890123456789]

用“s”表示的小分配。
大写字母表示的大量分配。
[0sssss678901GGGGFFFFEEEEDDDDCCCCBBBBAAAA]

现在,如果您释放所有大块,那么以后执行类似的大分配应该没有问题。
[0sssss6789012345678901234567890123456789]

问题是“大”和“小”是相对的,并且高度依赖于应用程序的性质。FastMem 定义了“大”和“小”之间的分界线。如果你碰巧有一些被 FastMem 归类为大的小分配,你可能会遇到以下问题。

[0sss4sGGGGsFFFFsEEEEsDDDDsCCCCsBBBBsAAAA]

现在,如果您释放剩下的大块:
[0sss4s6789s1234s6789s1234s6789s1234s6789]

并且尝试分配大于 400mb 的东西将会失败。


选项

  1. 您也许可以调整 FastMem 设置,以便 FastMem 也将您的所有“小”分配视为小。但是,在某些情况下这不起作用:
    • 您使用的为应用程序分配内存但绕过 FastMem 的任何 DLL 仍可能导致碎片。
    • 如果你不一起释放所有的大块,那么剩下的那些可能会导致碎片化,随着时间的推移会慢慢变得更糟。
  2. 您可以自己承担内存管理的任务。
    • 分配一个非常大的块,例如 3.5GB,您可以在应用程序的整个生命周期中保留它。
    • 您可以在设置新数组时确定要使用的指针位置,而不是使用动态数组。
  3. 当然,最简单的选择是 64 位。
  4. 您可以考虑替代数据结构。
    • 真的需要数组查找功能吗?如果不是,则以较小的块分配的另一个结构可能就足够了。
    • 即使您确实需要数组查找,也请考虑分页数组。稀疏数组是数组和链表的组合。数据存储在页面上,链接列表链接每个页面。
    • 一个简单的变体(因为您提到您的数组是二维的)将利用这一点:一维形成自己的数组,提供对第二维的多个数组之一的查找。
  5. 与备用数据结构选项相关,请考虑将一些数据存储在磁盘上。是的,性能会变慢。但如果能找到一种有效的缓存机制,那么可能就没有那么多了。最好慢一点,但不要崩溃。
于 2013-12-19T14:13:37.810 回答
0

动态数组在 Delphi 中是引用计数的,所以当它们不再使用时应该自动释放。与字符串一样,它们在共享/存储在多个变量/对象中时使用 COW(写入时复制)进行处理。所以看起来你有某种内存/引用泄漏(例如,内存中的对象仍然是对数组的引用)。只是为了确定:你没有做任何低级指针技巧,不是吗?

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

于 2013-12-19T08:39:02.237 回答