New和Dispose在内部执行什么操作?

6

我想知道在调用New和Dispose时内部会发生什么。Delphi帮助文档给出了一个合理的解释,但是如果一个人想要编写自己的New和Dispose,需要做些什么呢?这两种方法内部调用哪些方法,或者是否都是汇编语言实现的?

我并不想编写自己的New和Dispose。我只是非常好奇这两个方法的内部工作原理。

2个回答

10
New会执行以下操作:
  1. 使用调用GetMem为新对象分配内存。
  2. 初始化任何托管字段,如字符串、接口、动态数组等。
Dispose则相反:
  1. 终结托管字段。
  2. 使用调用FreeMem释放内存。
请注意,NewDispose都是内置函数。这意味着编译器对它们有额外的了解,并能够根据相关类型来确定它们的实现方式。
例如,如果类型没有托管字段,则New将优化为一个简单的GetMem调用。如果类型具有托管字段,则New将使用调用System._New,该函数执行上述步骤。 Dispose的实现方式也类似。对于非托管类型,可以使用一个简单的FreeMem调用,否则需要调用System._Dispose
现在,System._New的实现方式如下:
function _New(Size: NativeInt; TypeInfo: Pointer): Pointer;
begin
  GetMem(Result, Size);
  if Result <> nil then
    _Initialize(Result, TypeInfo);
end;

请注意,我刚刚展示了PUREPASCAL变量以便简化。调用GetMem足够简单。调用System._Initialize则要复杂得多。它使用TypeInfo参数查找对象中包含的所有托管类型并对其进行初始化。这是一个递归过程,因为例如记录可能包含自身是结构类型的成员。你可以在RTL源代码中看到所有细节。

至于System._Dispose,它调用System._Finalize然后调用FreeMem。而System._FinalizeSystem._Initialize非常相似,只不过它是用于终止托管类型而不是初始化它们。

长期以来,对于对性能敏感的Delphi用户来说,System._InitializeSystem._Finalize都是基于运行时类型信息实现的问题。这些类型在编译时已知,编译器可以被编写成内联初始化和终止,这将导致更好的性能。


谢谢David。是否能编写针对特定记录类型的更快的实现? - Blurry Sterk
1
对于托管类型,调用GetMem并清零内存通常会更快。对于非托管类型,没有任何好处。 - David Heffernan
我现在想我明白了。 - Blurry Sterk
我可以假设像SetLength()和Length()这样的内置函数也会被编译器以类似的方式处理,并且当使用SetLength()时,它们将指向诸如_SetLength()、_LStrSetLength()、_UStrSetLength()之类的过程,而对于使用Length()时,则会使用_DynArrayLength()函数,所有这些都取决于原始参数,如果需要找出SetLength()和Length()实际执行的操作,我可以研究这些方法的代码吗? - Blurry Sterk
调试代码将带领您找到相关的支持方法。 - David Heffernan

4

这是如何编写自己的New()Dispose()函数,并使用自己的记录初始化/终止:

uses
  TypInfo;

procedure RecordInitialize(Dest, TypeInfo: pointer);
asm
  {$ifdef CPUX64}
  .NOFRAME
  {$endif}
  jmp System.@InitializeRecord
end;

procedure RecordClear(Dest, TypeInfo: pointer);
asm
  {$ifdef CPUX64}
  .NOFRAME
  {$endif}
  jmp System.@FinalizeRecord
end;

function NewRec1(TypeInfo: pointer): pointer;
begin
  GetMem(result, GetTypeData(TypeInfo)^.RecSize);
  RecordInitialize(result, TypeInfo);
end;

function NewRec2(TypeInfo: pointer): pointer;
var
  len: integer;
begin
  len := GetTypeData(TypeInfo)^.RecSize;
  GetMem(result, len);
  FillChar(result^, len, 0);
end;

procedure NewDispose(Rec: pointer; TypeInfo: Pointer);
begin
  RecordClear(Rec, TypeInfo);
  FreeMem(Rec);
end;

首先,有一个低级汇编技巧可以调用我们需要的隐藏内部函数。
然后我提出了两种初始化记录的方法,一种使用System.@InitializeRecord,另一种使用FillChar。还要注意,如果您在动态数组中分配记录,则项目的初始化/完成将自动完成。并且初始化不会调用Initialize()而是使用FillChar将内存填充为零。
遗憾的是,我们无法定义具有通用参数的全局函数/过程,否则我们可能已经摆脱了TypeInfo()参数。
几年前,我重新编写了RTL部分的记录初始化/完成,以实现更快的执行。请注意,TObject将调用FinalizeRecord,因此它是RTL的一部分,可以进行优化。编译器应该发出代码而不是使用RTTI-但至少RTL应该再进行一些优化。
如果您使用我们的开源SynCommons.pas单元,并为您的项目定义了DOPATCHTRTL条件,那么您将拥有RTL FillChar Move RecordCopy FinalizeRecord InitializeRecord TObject.CleanupInstance低级函数的进程内补丁,该补丁将使用优化的汇编和SSE2操作码(如果可用)。

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