Delphi中对象的字符串共享/引用问题
我的应用程序根据文件名(包括其他基于字符串的信息)在内存中构建了很多对象。我希望通过分别存储路径和文件名来优化内存使用,然后在同一路径中的对象之间共享路径。我没有试图看看使用字符串池或任何东西,基本上我的对象被排序,所以如果我有10个对象具有相同的路径我希望对象2-10的路径“指向”对象1的路径(例如对象[2]。路径=对象[1]。路径);我认为我告诉他们(通过对象[2] .Path = object [1]),我不相信我的对象事实上共享对同一个字符串的引用。 .Path分配)。Delphi中对象的字符串共享/引用问题
当我用字符串列表进行实验并将所有值设置为列表中的第一个值时,我可以看到“内存保护”的作用,但是当我使用对象时,我完全没有看到任何变化,我承认我只使用任务管理器(私人工作集)来观察内存使用的变化。
这是一个人为的例子,我希望这是有道理的。
我有一个对象:
TfileObject=class(Tobject)
FpathPart: string;
FfilePart: string;
end;
现在我创建对象100万分的情况下,使用每一个新的字符串:
var x: integer;
MyFilePath: string;
fo: TfileObject;
begin
for x := 1 to 1000000 do
begin
// create a new string for every iteration of the loop
MyFilePath:=ExtractFilePath(Application.ExeName);
fo:=TfileObject.Create;
fo.FpathPart:=MyFilePath;
FobjectList.Add(fo);
end;
end;
运行这件事和任务经理说我使用68MB的内存什么的。 (请注意,如果我在循环之外分配了MyFilePath,那么由于字符串的一个实例,我确实节省了内存,但这是一个人为的例子,实际上并不是在应用程序中发生的情况)。
现在我想通过使所有对象共享的路径字符串的同一实例“优化”我的内存使用情况,因为它是相同的值:
变种X:整数;对于x:= 1到FobjectList.Count-1做 开始 做 开始 TfileObject(FobjectList [x])。FpathPart:= TfileObject(FobjectList [0])。FpathPart; 结束; 结束;
任务管理器显示没有变化。
但是,如果我做一个的TStringList类似的事情:
var x: integer;
begin
for x := 1 to 1000000 do
begin
FstringList.Add(ExtractFilePath(Application.ExeName));
end;
end;
任务管理器说60MB的内存使用。
现在与优化:
var x: integer;
begin
for x := 1 to FstringList.Count - 1 do
FstringList[x]:=FstringList[0];
end;
任务管理器显示内存使用量的下降,我所期望的,现在10MB。
所以我似乎能够在字符串列表中共享字符串,但不能在对象中共享字符串。我明显错过了一些概念上的代码或两者!
我希望这是有道理的,我真的可以看到使用这种技术来节省内存的能力,因为我有很多对象都有大量的字符串信息,数据以许多不同的方式排序,我希望成为能够在将这些数据加载到内存中后迭代这些数据,并通过以这种方式共享字符串来再次释放部分内存。
在此先感谢您提供的任何帮助。
PS:我用Delphi 2007,但我刚刚测试了德尔福2010年,其结果都是一样的,不同的是德尔福2010使用两倍的内存,由于unicode字符串...
当你的Delphi程序分配和释放内存时,它不是直接使用Windows API函数,而是通过内存管理器。你在这里观察到的事实是内存管理器在程序中不再需要时将所有分配的内存释放回操作系统。它将保留部分或全部以备后用,以加快应用程序中稍后的内存请求。因此,如果使用系统工具,内存将按程序分配列出,但它并未处于活动状态,它在内部标记为可用,并存储在MM将用于任何其他内存的可用内存块列表中在程序进入操作系统并请求更多内存之前,在您的程序中进行分配。
如果要真正检查程序的任何更改如何影响内存消耗,则不应依赖外部工具,而应使用内存管理器提供的诊断程序。下载完整的FastMM4版本并将其作为DPR文件中的第一个单元用于您的程序中。您可以使用GetMemoryManagerState()
函数获取详细信息,该函数将告诉您使用了多少个小型,中型和大型内存块以及为每个块大小分配了多少内存。然而,对于快速检查(这里将完全充分),您可以简单地调用GetMemoryManagerUsageSummary()
函数。它会告诉你分配的内存总量,如果你打电话给你,你会看到你的重新分配FPathPart
确实释放了几MB的内存。
当使用TStringList
时,您会观察到不同的行为,并且所有字符串都按顺序添加。这些字符串的内存将从较大的块中分配,而这些块将不包含其他内容,因此可以在释放字符串列表元素时再次释放它们。如果OTOH创建了对象,那么这些字符串将与其他数据元素交替分配,因此释放它们将在较大的块中创建空的内存区域,但这些块不会被释放,因为它们还包含其他内容的有效内存。你基本上增加了内存碎片,这本身可能是一个问题。
因为任务管理器不告诉你全部的事实。与此代码比较:
var
x: integer;
MyFilePath: string;
fo: TfileObject;
begin
MyFilePath:=ExtractFilePath(Application.ExeName);
for x := 1 to 1000000 do
begin
fo:=TfileObject.Create;
fo.FpathPart:=MyFilePath;
FobjectList.Add(fo);
end;
end;
Pham,谢谢,我知道这个。这绝对有效,每个对象只存储一个“MyFilePath”实例和大量减少的内存使用量。我正在看的是在第一次分配之后设置路径。其中一个主要原因是我有很多从流中加载的对象,并且当时每个对象都有自己的版本。我只是希望迭代列表并将它们设置为字符串的“共享”版本。任务管理器可以识别Tstringlist发生这种情况的时间,而不是对象。 – Jenakai 2010-03-13 22:57:43
正如另一个答案所指出的那样,未被使用的内存并不总是通过Delphi内存管理器立即释放到系统中。
您的代码通过动态增长对象列表来保证大量此类内存。
甲TObjectList(共同与的TList和的TStringList)使用增量内存分配器。其中一个容器的新实例以分配给4个项目的内存(容量)开始。当添加的项目数量超过容量分配额外的内存时,最初将容量加倍,然后一旦达到一定数量的项目,则将容量增加25%。
每次计数超过容量,附加的存储器分配,当前存储器复制到新的存储和释放的先前使用的存储器(它是这个存储器,它不立即返回到系统)。
当你知道有多少项目将被加载到这些类型的列表,你可以避开这个内存重新分配行为(和实现显著的性能提升),通过相应预分配列表容量之一。
您不一定必须设置所需的精确容量 - 最佳猜测(更可能接近或高于实际需要的数字仍然会优于初始缺省容量4如果项目的数量显着> 64)
+1这个不错的提示! – jpfollenius 2010-03-15 09:21:04
要共享引用,需要直接指定字符串并且类型相同(很明显,您不能在UnicodeString和AnsiString之间共享引用)。
我能想到的达到你想要什么,最好的方法是如下:
var StrReference : TStringlist; //Sorted
function GetStrReference(const S : string) : string;
var idx : Integer;
begin
if not StrReference.Find(S,idx) then
idx := StrReference.Add(S);
Result := StrReference[idx];
end;
procedure YourProc;
var x: integer;
MyFilePath: string;
fo: TfileObject;
begin
for x := 1 to 1000000 do
begin
// create a new string for every iteration of the loop
MyFilePath := GetStrReference(ExtractFilePath(Application.ExeName));
fo := TfileObject.Create;
fo.FpathPart := MyFilePath;
FobjectList.Add(fo);
end;
end;
要确保它工作正常,你可以调用StringRefCount(单位系统)功能。我不知道引入了哪个版本的delphi,所以这是当前的实现。
function StringRefCount(const S: UnicodeString): Longint;
begin
Result := Longint(S);
if Result <> 0 then
Result := PLongint(Result - 8)^;
end;
让我知道它是否按照你想要的那样工作。
编辑:如果你怕的StringList增长过大的,你可以放心地定期扫描,并从列表中的任何字符串的1
列表中删除StringRefCount可以擦拭干净太...但是,这将使该函数保留传递给该函数的任何新字符串的新副本。
感谢mghie,这解释了由于TstringList和对象之间的不同行为而导致的答案。 – Jenakai 2010-03-14 00:18:30