System.Move和String数组

时间:2016-03-18 13:56:36

标签: string delphi

我正在尝试将一些数组元素(字符串)移动到其他位置。

当我使用System.Move()时,FastMM4报告泄漏。

这是一个显示问题的小片段。

procedure TForm1.Button2Click(Sender: TObject);
type
  TArrayOfStr = array of string;
const
  Count1 = 100;
  String1 = 'some string ';  {space at end}
var
  Array1: TArrayOfStr;
  Index1: Integer;
begin
  SetLength(Array1, Count1);
  Index1 := 0;
  while Index1 < Count1 do begin
    Array1[Index1] := String1 + IntToStr(Index1);
    Inc(Index1);
  end;
  System.Move(Array1[0], Array1[3], 2 * SizeOf(string)); {move 2 cells from cell 0 to cell 3}
  ShowMessage(Array1[3]);
end;

这可能与SizeOf(String)有关,但我不知道是什么。

有人可以帮助我消除泄漏。

4 个答案:

答案 0 :(得分:4)

<强>问题
您遇到的问题与字符串的引用计数有关。

  1. 泄漏
    如果,您所覆盖的区域中已存在字符串,则不会释放这些字符串。这些是您报告的泄漏。

  2. 潜在的访问权限 您复制字符串指针,但不增加字符串的引用计数。如果原始字符串由于超出范围而被破坏,则会导致访问冲突。这是一个非常微妙的错误,当你最不期望它时会咬你。

  3. 最佳解决方案
    让Delphi进行复制,然后所有内部簿记都能正常完成,这简单得多。

      {move 6 from cell 1 to cell 3}
      System.Move(Array1[0], Array1[3], 2 * SizeOf(string)); 
      //This does not increase the reference count for the string;
      //leading to problems at cleanup.
    
      Array1[3]:= Array1[0];
      Array1[4]:= Array1[1];  //in a loop obviously :-)
      //this increases the reference count of the string.
    

    请注意,Delphi不会复制字符串,只是复制指针并根据需要增加引用计数。它还可以根据需要释放任何字符串。

    黑客解决方案
    您应该先手动清除该区域 使用

    for i:= start to finish do Array1[i]:= '';
    

    解决方案的下一部分可怕的黑客是手动增加您复制的字符串的引用次数。
    请参阅:http://docwiki.embarcadero.com/RADStudio/Seattle/en/Internal_Data_Formats#Long_String_Types

    procedure IncreaseRefCount(const str: string; HowMuch: integer = 1);
    var
      Hack: PInteger;
    begin
      Hack:= pointer(str);
      Dec(Hack,2); //get ref count
      Hack^:= AtomicIncrement(Hack^,HowMuch);
    end;
    
      System.Move(Array1[0], Array1[3], 2 * SizeOf(string)); 
      IncreaseRefCount(Array1[3]);
      .... do this for every copied item.
    

    请注意,如果从阵列外的某个位置获取字符串,则此hack不是完全线程安全的。

    然而,如果你真的非常需要速度,那么它可能是一个解决方案,可以获得2%的副本性能。

    警告
    不要使用此代码手动减少引用计数,您将遇到线程安全问题!

    需要速度
    简单地处理一些字符串不太可能导致缓慢 如果您坚持使用托管字符串,则无法摆脱清理问题 另一方面,引用计数的开销实际上并没有那么糟糕,所以我怀疑缓慢的原因在于其他地方;在某个地方,我们无法看到,因为您还没有告诉我们您的问题。

    我建议你问一个新的问题,解释你要做的事情,为什么以及在哪里慢慢伤害你。

答案 1 :(得分:3)

Delphi中的String类型是托管类型。 Delphi会记录引用和解除引用,并在不再引用时自动释放分配给字符串的内存。

泄漏的原因是您绕过了Delphi对字符串类型的管理。您只是覆盖引用数组中第4个字符串的指针。 (因为2 * ...因此也是第5个)所以你现在在内存中有不再被引用的字符串。但Delphi没有意识到这一点,也无法释放内存。

解决方案:写下Array1[3] := Array1[0];

编辑:

我完全回答了你的问题;为您提供所需的一切:1)了解您为什么会出现内存泄漏。 2)并使泄漏消失。

然而,你并不满意......在评论中你已经解释过你正试图通过人工基准来改善你想象出来的幻影性能问题。

  • 已向您解释,Move比正常字符串赋值快一点的唯一原因是:字符串赋值需要执行额外的记录保存防止内存泄漏和访问冲突
  • 如果你坚持使用Move,你需要自己做记录。 (约翰甚至演示了how。)
  • 然后你抱怨这会减慢你的Move“解决方案”。
  • 说真的,接受你的选择:如果你想使用string,你可以让它快一点AV和内存泄漏 稍微慢一些但行为正确。没有魔杖挥舞着它会为你解决它。
  • 可以选择放弃字符串类型(以及它为您提供的所有优点)。即请改用 char 数组。当然,您必须自己完成所有内存管理,并查看多字节字符串复制对您有多大帮助。
  

我仍然坚持认为,如果您提出一个新问题来证明您尝试解决的具体性能问题,您将获得更好的反馈。

E.g。在评论中,您提到过您正试图提高TStringList的效果。

以前在旧版本的Delphi中遇到TStringList的性能问题。即使使用数十万件物品,它也一般都很好。但是,即使只有10,000个字符串,CommaText显然也很慢;几乎无法忍受40,000。

解决方案并没有涉及尝试滥用引用计数:因为这不是它缓慢的原因。它很慢,因为算法有点天真,并执行了大量的增量内存分配。编写自定义CommaText方法解决了这个问题。

答案 2 :(得分:0)

我正在添加一个根本不同的答案,其中我提供了一个选项,您可以在 而不是 完成任务时真正做到这一点。

  

免责声明:我在这里描述的是糟糕的建议,不应该这样做(但它会起作用)。请随意向它学​​习,但 不要使用它

正如已经讨论过的那样令人作呕,你的问题是,当你使用Move时,你会绕过引用计数。

  • 解决方案涉及通过处理主体来进行“内部”引用计数,无论内部引用字符串多少次,您只需对字符串保留一个计数。
  • 当您确定没有对该字符串的“内部”引用时,您将只删除该单个计数。
  • 不幸的是,放弃了内部引用计数,唯一可以确定这一点的就是你完全使用了内部数组。
  • 此时您必须先手动清除内部阵列。
  • 然后将先前使用过的所有字符串的引用计数减少1。
  • 这意味着您需要为每个字符串单独的“主引用”。
  

警告
  有两个直接问题:

     
      
  • 您需要第二个列表来跟踪主引用计数;浪费记忆。
  •   
  • 在完全使用内部数组之前,您无法恢复内部不再引用的字符串使用的内存,因为您已放弃跟踪内部引用计数的能力
      (从技术上讲,这仍然是内存泄漏,虽然是可控制的。如果正确清理,FastMM将不报告它。)
  •   

不用多说,一些示例代码:

//NOTE: Deliberate use of fixed size array instead of dynamic to
//avoid further complications. See Yet Another Warning after code
//for explanation and resolution.
TStringArray100 = array[0..99] of string;
TBadStrings = class(TObject)
private
  FMasterStrings: TStrings;
  FInternalStrings: TStringArray100;
public
  ...
end;

constructor TBadStrings.Create()
begin
  FMasterStings := TStringList.Create;
end;

destructor TBadStrings.Destroy;
begin
  Clear;
  FMasterStrings.Free;
  inherited;
end;

procedure TBadStrings.Clear;
begin
  for I := 0 to 99 do
    Pointer(FInternalStrings[I]) := 0;

  //Should optimise to equivalent of
  //Move(0, FInternalStrings[0], 100 * SizeOf(String));

  //NOTE: Only now is it safe to clear the master list.
  FMasterStings.Clear;
end;

procedure TBadStrings.SetString(APos: Integer; AString: string);
begin
  FMasterStrings.Add(AString); //Hold single reference count

  //Bypass reference counting to assign the string internally
  //Equivalent to Move
  Pointer(FInternalStrings[APos]) := Pointer(AString);
end;

//Usage
begin
  LBadStrings := TBadStrings.Create;
  try
    for I := 0 to 199 do
    begin
      //NOTE 0 to 99 are set, then all overwritten with 100 to 199
      //However strings 0 to 99 are still in memory...
      LBadStrings.SetString(I mod 100, 'String ' + IntToStr(I));
    end;
  finally
    //...until cleanup.
    LBadStrings.Free;
  end;
end;

注意:您可以使用Move上的FInternalStrings添加方法以执行任何操作。无法跟踪这些引用并不重要,因为主引用可以在末尾执行正确的清理 。然而....

  

警告:您对FInternalStrings 必须 所做的任何操作都会绕过引用计数,否则会产生令人讨厌的副作用。所以不用说你需要坚定地保护对内部阵列的访问。如果客户端代码可以直接访问数组,那么您可能会意外“滥用”。

     

又一个警告:正如在代码中所评论的那样,它使用固定大小的数组来避免其他问题。所述问题是如果使用动态数组,则调整数组大小可以应用引用计数。增加数组大小不应该是一个问题(我记得是一个指针副本)。但是,当大小减小时,将根据需要取消引用被丢弃的元素。这意味着在缩小数组之前,你必须采取预先设置这些元素的第一个指针。

以上是一种可以以受控方式绕过字符串引用计数的方法。但是,让我重申它是一个多么可怕的想法。

你应该毫不费力地编造一个人工基准来证明它更快。但是,我非常怀疑它会在 现实世界 环境中提供任何好处。
事实上,如果确实如此,你可能完全有不同的问题;因为在地球上为什么要把字符串拖延到那么多,以至于花在那里的时间会影响你应用程序的其他方面呢?

答案 3 :(得分:-2)

这是我提出的解决方案。它受System.Move()的启发。

据我所知,经过多次测试后,似乎工作正常 - 没有FastMM4报告泄漏。

Obvioulsy,这不是我追求的手工优化的asm例程;但考虑到我在asm地区的(缺乏)才能,现在必须这样做。

如果您对此发表评论,我将非常感激 - 尤其要指出任何陷阱,以及任何其他(例如速度)改进。

{ACount refers to the number of actual array elements (cells of strings), not their byte count.}
procedure MoveString(const ASource; var ATarget; ACount: NativeInt);
type
  PString = ^string;
const
  SzString = SizeOf(string);
var
  Source1: PString;
  Target1: PString;
begin
  Source1 := PString(@ASource);
  Target1 := PString(@ATarget);
  if Source1 = Target1 then Exit;
  while ACount > 0 do begin
    Target1^ := Source1^;
    //Source1^ := ''; {enable if you want to avoid duplicates}
    Inc(Source1);
    Inc(Target1);
    Dec(ACount);
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
type
  TArrayOfStr = array of string;
const
  Count1 = 100;
  String1 = 'some string ';  {space at end}
var
  Array1: TArrayOfStr;
  Index1: Integer;
begin
  SetLength(Array1, Count1);
  Index1 := 0;
  while Index1 < Count1 do begin
    Array1[Index1] := String1 + IntToStr(Index1);
    Inc(Index1);
  end;
  MoveString(Array1[0], Array1[3], 2); {move 2 cells from cell 0 to cell 3}
  ShowMessage(Array1[3]); {should be 'some string 0'}
  MoveString(Array1[3], Array1[0], 2); {move 2 cells from cell 3 to cell 0}
  ShowMessage(Array1[0]); {should be 'some string 0'}
end;