注意:32位应用程序,不计划迁移到64位。
我正在处理一个非常消耗内存的应用程序,并且已经优化了所有与内存分配/取消分配相关的路径。(在应用程序本身AFAIK和测试中没有内存泄漏,没有句柄泄漏,没有任何其他类型的泄漏。我不能接触的第三方库当然是候选的,但在我的情况下不太可能)
该应用程序将经常分配大的一维和二维动态数组,其中包含最多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.5 GB或更多的内存才能处理时,就会出现大问题。在恢复之前,我当前的代码将其自身限制为1.5 GB已用内存的上限,但在这种情况下,我必须将限制降至1 GB以下,这基本上会使应用程序在每次操作后恢复自身,甚至不能保证一切正常。
如果另一个操作需要处理更大的数据集,并且总共需要4GB或更多的内存,该怎么办?
需要注意的是,我说的不是实际的4GB内存,而是通过分配巨大的动态阵列来消耗内存,操作系统在取消分配后不会收回这些阵列,因此它仍然认为它被消耗了,所以它加起来了。
因此,我的下一个攻击点是强制fastmm向操作系统释放所有(或至少部分)内存。我在这里专门针对巨大的动态数组。同样,这些都在第三方库中,因此重新编码并不是最重要的选项。修改fastmm代码并编写一个释放内存的proc会更容易、更快。
我无法从FastMM切换,因为目前整个应用程序和一些第三方库都围绕PushAllocationGroup的使用进行了大量编码,以便快速查找和精确定位任何内存泄漏。我知道我可以编写一个伪FastMM单元来解决编译引用,但我将没有这种快速而可靠的泄漏检测。
总之:有什么办法可以迫使FastMM向操作系统发布至少一些大的块吗?(当然有,实际的问题是:有人写过吗?如果有,分享思想?)
感谢
后期编辑:
我很快就会提出一个小型的相关测试应用程序模拟一个似乎没那么容易
我怀疑这个问题实际上是由FastMM引起的。对于巨大的内存块,FastMM不会进行任何子分配。您的分配请求将使用直接的VirtualAlloc
进行处理。则解除分配为VirtualFree
。
这是假设您在一个连续的块中分配这些380MB的对象。我怀疑您实际拥有的是粗糙的2D动态阵列。而且它们不是单一的分配。5000x5000个参差不齐的2D动态阵列需要5001个分配来初始化。一个用于行指针,5000个用于行。这些将是中等FastMM块。将进行分拨。
我觉得你要求太高了。根据我的经验,任何时候你在一个32位的过程中需要超过3GB的内存,游戏就结束了。地址空间的碎片会在内存耗尽之前阻止您。你不能指望这会奏效。切换到64位,或者使用更聪明、要求更低的分配模式。或者你真的需要密集的2D阵列吗?你能使用稀疏存储吗?
如果不能通过这种方式减少内存需求,可以使用内存映射文件。这将允许您利用64位系统所拥有的额外内存。系统的磁盘缓存可能大于4GB,因此您的应用程序可以遍历超过4GB的内存,而无需实际访问磁盘。
你当然可以尝试不同的内存管理器。老实说,我不希望这会有所帮助。您可以编写一个使用HeapAlloc
的简单的替换内存管理器。并启用低碎片堆(在Vista上默认启用)。但我真诚地怀疑它是否会有所帮助。恐怕你不会有一个快速的解决办法。为了解决这个问题,您需要对代码进行更根本的修改。
正如其他人所说,您的问题很可能是由于内存碎片造成的。您可以通过使用VirtualQuery创建一个内存分配给应用程序的图片来测试这一点。您很可能会发现,尽管您可能有足够的总内存用于新阵列,但您没有足够的连续内存。
FastMem已经做了很多工作来避免内存碎片带来的问题。"小"分配在地址空间的低端完成,而"大"分配在高端完成。这避免了一个常见的问题,即一系列先大后小的分配,然后释放所有的大分配,导致大量碎片内存几乎不可用。(当然,任何比最初的大额分配稍大的东西都无法使用。)
要了解FastMem方法的灵活性,请想象您的内存布局如下:
每个数字代表一个100mb的块[0123456789012345678901234567890123456789]
以"s"表示的小额拨款
用大写字母重复的大额拨款[0sssss678901GGGGFFFFEEEEDDDDCCCCBBBBAAAA]
现在,如果您释放了所有的大块,那么以后执行类似的大分配应该不会有问题[0sssss6789012345678901234567890123456789]
问题是"大"one_answers"小"是相对的,并且高度依赖于应用程序的性质。FastMem定义了"大"one_answers"小"之间的分界线。如果你碰巧有一些小的分配,FastMem会将其归类为大的分配,你可能会遇到以下问题。
[0sss4sGGGGsFFFFsEEEEsDDDDsCCCCsBBBBsAAAA]
现在,如果您释放了剩下的大块:[0sss4s6789s1234s6789s1234s6789s1234s6789]
而分配大于4亿字节的内存的尝试将失败。
选项
- 您可以调整FastMem的设置,使您的所有"小"分配也被FastMem视为小。然而,在一些情况下,这是行不通的:
- 您使用的任何为应用程序分配内存但绕过FastMem的DLL仍可能导致碎片
- 如果不将所有大块放在一起,剩下的大块可能会导致碎片化,随着时间的推移,碎片化会慢慢恶化
- 你可以自己承担记忆管理的任务。
- 分配一个非常大的块,例如3.5GB,您可以在应用程序的整个生命周期中保留它
- 您可以确定设置新数组时要使用的指针位置,而不是使用动态数组
- 当然,最简单的选择是使用64位
- 您可以考虑其他数据结构。
- 您真的需要数组查找功能吗?如果没有,那么另一个以较小的块进行分配的结构可能就足够了
- 即使您确实需要数组查找,也可以考虑分页数组。稀疏数组是数组和链表的组合。数据存储在页面上,链接列表链接每个页面
- 一个简单的变体(因为你提到你的数组是二维的)是利用它:一个维度形成自己的数组,为第二个维度提供对多个数组之一的查找
- 与备用数据结构选项相关,请考虑在磁盘上存储一些数据。是的,性能会变慢。但是,如果能够找到一种高效的缓存机制,那么可能就没有那么多了。速度慢一点会更好,但不要崩溃
动态数组在Delphi中被引用计数,因此当不再使用时,它们应该自动释放。与字符串一样,当共享/存储在多个变量/对象中时,使用COW(写时复制)处理它们。因此,似乎存在某种内存/引用泄漏(例如,内存中保持静止的对象是对数组的引用)。可以肯定的是:你没有做任何低级别的指针技巧,不是吗?
所以请是的,发布一个测试程序(或通过电子邮件发送完整的程序),这样我们中的一个人就可以看一看了。