为什么在Delphi(带有Unicode)中使用SetString需要更少的内存?

8
这是Delphi 2009,因此适用Unicode。
我有一些代码,它将字符串从缓冲区加载到StringList中,如下所示:
      var Buffer: TBytes; RecStart, RecEnd: PChar; S: string;

      FileStream.Read(Buffer[0], Size);

      repeat
         ... find next record RecStart and RecEnd that point into the buffer;        

         SetString(S, RecStart, RecEnd - RecStart);
         MyStringList.Add(S);
      until end of buffer

但在进行一些修改时,我更改了我的逻辑,结果我添加了相同的记录,但是它们是作为单独派生的字符串添加的,而不是通过SetString添加的,例如:

      var SRecord: string;

      repeat
        SRecord := '';
        repeat
          SRecord := SRecord + ... processed line from the buffer;
        until end of record in the buffer

        MyStringList.Add(SRecord);
      until end of buffer

我注意到StringList的内存使用从52MB增加到了约70MB,增加了超过30%。

为了恢复低内存使用率,我发现必须使用SetString创建字符串变量并将其添加到StringList中,如下所示:

      repeat
        SRecord := '';
        repeat
          SRecord := SRecord + ... processed line from the buffer;
        until end of record in the buffer

        SetString(S, PChar(SRecord), length(SRecord));
        MyStringList.Add(S);
      until end of buffer

检查和比较S和SRecord,它们在所有情况下都完全相同。但是将SRecord添加到MyStringList中所使用的内存比添加S要多得多。
有人知道发生了什么以及为什么SetString可以节省内存吗?
跟进。我没想到会这样,但我检查了一下,确保没有。
  SetLength(SRecord, length(SRecord));

nor

  Trim(SRecord);

释放多余的空间。似乎需要使用SetString来完成此操作。

1
顺便说一下,这是我在StackOverflow上的第100个问题。我想感谢这里的编程社区,在过去的两年中给予我的巨大帮助,解决了我的编程问题。 - lkessler
2个回答

15
如果您将字符串连接起来,则内存管理器将分配更多的内存,因为它假设您会添加越来越多的文本并为未来的连接分配额外的空间。因此,字符串的分配大小要比实际使用的大小大得多(取决于所使用的内存管理器)。如果您使用SetString,则新字符串的分配大小几乎与实际使用的大小相同。当SRecord字符串超出范围且其引用计数为零时,占用SRecord的内存将被释放。因此,您最终将得到所需字符串的最小分配大小。

听起来很有道理,@Andreas,但多了30%的内存吗?我的字符串很长,平均每个字符串有500个字符,我要加载100,000个字符串。也许需要40次连接才能构建一个字符串。然后,“SRecord”字符串会被重用于下一个记录,所以我希望内存管理器会重用空间。我可以从你的解释中理解2或3%,但不是30%。 - lkessler
为什么内存管理器应该重用SRecord字符串,如果您仍然从StringList引用它。它不能这样做,必须为每个创建一个新的SRecord字符串。 - Andreas Hausladen
在我所有的示例中,这两行代码都在一个循环中,每个记录循环一次或循环10万次。在循环开始时,我将SRecord:=“”设置为空字符串,然后循环处理记录中的行并将它们附加到SRecord中。因此,SRecord只有大约500个字符长。对于下一个记录,我认为将SRecord设置为空字符串将允许内存管理器进行清理。让我更新示例以显示循环。 - lkessler
3
将“”分配给SRecord并不会释放该字符串,因为它的引用计数为2(@SRecord和@TStringList.FList[].FString),它只将RefCount减少到1(@TStringList.FList[].FString)并将SRecord变量设置为null。因此,超大的连接字符串仍然存在并由StringList使用。您的代码在每次迭代中都会分配一个新的SRecord字符串。每当连接的内容超出字符串的分配大小时,您的字符串增长至少100% + 31字节(用于小块),增长25%用于中块,并增长25%,上限为64KB(用于大块)(FastMM)。 - Andreas Hausladen
@Andreas:那么你的意思是,在我的第一组代码中,StringList使用了过度分配的连接字符串。在第三组代码中,创建了一个精确大小的新S,然后由内存管理器释放了过度分配的字符串,因为StringList使用的是S而不是SRecord。如果是这样的话,那就非常微妙了。 - lkessler
@lkessler:也许我太过于技术化了,但那就是我想要告诉你的。 - Andreas Hausladen

-1
尝试安装内存管理器过滤器(Get/SetMemoryManager),它将所有对GetMem/FreeMem的调用传递给默认内存管理器,但它还执行统计信息收集。您可能会发现两种变体在内存消耗方面是相等的。
这只是内存碎片化问题。

除了他说一个代码变体正在过度分配内存,这是一种内存碎片化。 - Alex
碎片化是指内存可用,但只有小块。这不是过度分配。 - himself
@himself 碎片化的一个定义是“在没有意图使用它的情况下分配存储空间”,另一个定义是“当空闲存储空间被分成许多小块时”。这正是发生的情况。从调用代码的角度来看:内存是可用的(因为我没有调用MemManager.GetMem并且我在那个位置上没有分配内存),但仍然无法重新使用。如果不是碎片化,那是什么呢? - Alex
你说的有道理,但那不是你最初的观点。像那样分散的内存将被GetMem/FreeMem视为已获取,因此内存消耗不会相等。这不是分段分配的情况,而是预先分配超过所需的内存。 - himself

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