Delphi中对象的字符串共享/引用问题

时间:2010-03-13 20:18:49

标签: delphi

我的应用程序根据文件名(以及其他基于字符串的信息)在内存中构建许多对象。我希望通过分别存储路径和文件名来优化内存使用,然后在同一路径中共享对象之间的路径。我没有尝试使用字符串池或任何东西,基本上我的对象是排序的,所以如果我有10个具有相同路径的对象我希望对象2-10让它们的路径“指向”对象1的路径(例如对象) [2]。路径=对象[1]。路径);

我有一个问题,我不相信我的对象实际上在我认为我告诉他们之后共享对同一个字符串的引用(通过对象[2] .Path = object [1]。路径分配)。

当我使用字符串列表进行实验并将所有值设置为指向列表中的第一个值时,我可以看到“内存保护”正在运行,但是当我使用对象时,我看到完全没有任何变化,诚然,我只使用任务管理器(私人工作集)来监视内存使用的变化。

这是一个人为的例子,我希望这是有道理的。

我有一个对象:

TfileObject=class(Tobject)
  FpathPart: string;
  FfilePart: string;
end;

现在我创建了1,000,000个对象实例,每个实例使用一个新字符串:

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,那么我会因为1个字符串实例而节省内存,但这是一个人为的例子而不是实际上它在应用程序中会如何发生)。

现在我想通过让所有对象共享路径字符串的相同实例来“优化”我的内存使用量,因为它是相同的值:

var 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,但我刚刚在Delphi 2010上进行了测试,结果是一样的,只是因为unicode字符串,Delphi 2010使用了两倍的内存......

4 个答案:

答案 0 :(得分:4)

当您的Delphi程序分配和释放内存时,它不是直接使用Windows API函数,而是通过内存管理器。你在这里观察的是,当程序中不再需要时,内存管理器不会将所有已分配的内存释放回操作系统。它将部分或全部分配给以后,以加快应用程序中的后续内存请求。因此,如果你使用系统工具,内存将被列为由程序分配,但它没有被激活使用,它被标记为内部可用,并存储在可用内存块的列表中,MM将用于任何进一步的内存程序中的分配,在进入操作系统并请求更多内存之前。

如果您想真正检查程序的任何更改如何影响内存消耗,则不应依赖外部工具,而应使用内存管理器提供的诊断。下载完整的FastMM4版本,并将其作为DPR文件中的第一个单元在程序中使用。您可以使用GetMemoryManagerState()函数获取详细信息,该函数将告诉您使用了多少小型,中型和大型内存块以及为每个块大小分配了多少内存。但是,为了快速检查(这里完全足够),您只需调用GetMemoryManagerUsageSummary()函数即可。它会告诉你总分配的内存,如果你调用它,你会看到你FPathPart的重新分配确实释放了几MB的内存。

当使用TStringList时,您将观察到不同的行为,并且所有字符串都是按顺序添加的。这些字符串的内存将从较大的块中分配,并且这些块将不包含任何其他内容,因此可以在释放字符串列表元素时再次释放它们。如果您创建对象的OTOH,那么字符串将与其他数据元素交替分配,因此释放它们将在较大的块中创建空的内存区域,但是块不会被释放,因为它们包含其他东西的有效内存。你基本上已经增加了内存碎片,这可能本身就是一个问题。

答案 1 :(得分:1)

正如另一个答案所指出的那样,Delphi内存管理器并不总能立即将未使用的内存释放到系统中。

您的代码通过动态增长对象列表来保证大量此类内存。

TObjectList (与 TList TStringList 相同)使用增量内存分配器。其中一个容器的新实例以分配给4个项目的内存开始(容量)。当添加的项目数超过容量时,会分配额外的内存,最初通过将容量加倍,然后一旦达到一定数量的项目,将容量增加25%。

每次计数超过容量时,会分配额外的内存,将当前内存复制到新内存并释放以前使用的内存(这是内存中的哪一个)没有立即返回系统。)

当您知道要将多少项加载到其中一种类型的列表中时,您可以通过预先分配容量来避免此内存重新分配行为(并实现显着的性能提升)相应的列表。

您不一定要设置所需的精确容量 - 最佳猜测(更可能更接近或更高,所需的实际数字仍然会比初始的默认容量更好4,如果项目数量显着> 64)

答案 2 :(得分:0)

因为任务经理没有告诉你全部真相。与此代码比较:

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;

答案 3 :(得分:0)

要共享引用,需要直接分配字符串并且类型相同(显然,您不能在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;

让我知道它是否有效。

编辑:如果您害怕字符串列表变得太大,您可以安全地定期扫描它并从列表中删除StringRefCount为1的任何字符串。

列表也可以擦除干净......但这会使函数保留传递给函数的任何新字符串的新副本。