在Delphi中检索返回值是否有任何优化?

时间:2010-01-19 00:42:10

标签: delphi optimization return-value variable-assignment

我试图通过使用存储字节的记录并通过使用具有相同功能的函数访问另一个记录的字段来找到一种优雅的方式来访问程序的其他部分中某些对象的字段将name命名为记录的字段。

TAilmentP = Record // actually a number but acts like a pointer
private
  Ordinal: Byte;
public
  function Name: String; inline;
  function Description: String; inline;
  class operator Implicit (const Number: Byte): TAilmentP; inline;
End;

 TSkill = Class
   Name: String;
   Power: Word;
   Ailment: TAilmentP;
 End;

class operator TAilmentP.Implicit (const Number: Byte): TAilmentP;
begin
  Result.Ordinal := Number;
  ShowMessage (IntToStr (Integer (@Result))); // for release builds
end;

function StrToAilment (const S: String): TAilmentP; // inside same unit
var i: Byte;
begin
  for i := 0 to Length (Ailments) - 1 do
    if Ailments [i].Name = S then
    begin
      ShowMessage (IntToStr (Integer (@Result))); // for release builds
      Result := i; // uses the Implicit operator
      Exit;
    end;
  raise Exception.Create ('"' + S + '" is not a valid Ailment"');
end;

现在,我试图通过重载转换运算符来使我的生活更轻松,这样当我尝试将一个字节分配给TAilmentP对象时,它会将其分配给Ordinal字段。 但是,正如我所检查的那样,这种尝试在性能方面似乎实际上是昂贵的,因为对隐式“运算符”的任何调用都将为返回值创建一个新的TAilmentP对象,执行其业务,然后返回值和将字节副本复制回调用它的对象,因为地址不同。

说实话,我的代码调用了这个方法,看起来这比仅仅将我的值直接赋值给对象的Ordinal字段要慢。

有没有办法让我的程序通过使用ANY方法/函数直接将值分配给我的字段?即使内联似乎也不起作用。有没有办法返回对(记录)变量的引用,而不是对象本身? 最后(抱歉有点偏离主题),为什么通过静态函数完成运算符重载?是不是让它们实例方法更快,因为你可以访问对象字段而不解除引用它们?这在我和我的代码的其他部分真的很方便。

[EDIT]这是Implicit运算符的汇编代码,其中包含所有优化,没有调试功能(甚至断点的“调试信息”)。

add al, [eax] /* function entry */
push ecx
mov [esp], al /* copies Byte parameter to memory */
mov eax, [esp] /* copies stored Byte back to register; function exit */
pop edx
ret

更有趣的是,下一个函数在启动时有一个mov eax,eax指令。现在看起来非常有用。 :P哦是的,我的Implicit运算符也没有内联。

我非常相信[esp]是Result变量,因为它的地址与我指定的地址不同。关闭优化后,[esp]将替换为[ebp- $ 01](我指定的内容)和[ebp- $ 02](字节参数),再添加一条指令将[ebp- $ 02]移动到AL(然后把它放在[ebp- $ 01]中,冗余的mov指令仍然在[epb- $ 02]。

我做错了什么,或者Delphi没有返回值优化?

3 个答案:

答案 0 :(得分:3)

通过寄存器返回适合寄存器的返回类型 - 偶数记录。只有更大的类型在内部转换为“out”参数,并通过引用传递给函数。

您的记录大小为1.复制记录的速度与制作普通Byte的副本一样快。

您为观察Result变量的地址而添加的代码实际上是伤害优化器。如果不要求变量的地址,则编译器不需要为其分配任何内存。变量只能存在于寄存器中。当你要求地址时,编译器需要分配堆栈内存,以便它有一个地址给你。

摆脱“释放模式”代码,而不是观察编译器在CPU窗口中的工作。您应该能够观察到您的记录主要存在于寄存器中。 Implicit运算符甚至可以编译为无操作,因为输入和输出寄存器都应该是EAX。


运算符是实例方法还是静态方法没有太大区别,特别是在性能方面。实例方法仍然会接收对它们被调用的实例的引用。这只是一个问题,即引用是否具有您选择的名称,或者它是否被称为Self并且是隐式传递的。虽然你不必写“自我”。在你的字段访问之前,Self变量仍然需要像静态运算符方法的参数一样被解除引用。

我要说的其他语言的优化就是你应该查找名为命名的返回值优化或其缩写NRVO。之前已经在Stack Overflow上进行了介绍。它与内联无关。

答案 1 :(得分:1)

Delphi应该通过使用指针来优化返回赋值。对于C ++和其他OOP编译语言也是如此。在引入运算符重载之前我停止编写Pascal,所以我的知识有点过时了。以下是我会尝试的内容:

我在想的是......你能在堆上创建一个对象(使用New)并从你的“Implicit”方法中传回指针吗?这应避免不必要的开销,但会导致您将返回值作为指针处理。重载你的方法来处理指针类型?

我不确定你是否可以通过内置的运算符重载来实现这一点。就像我提到的那样,重载是我在Pascal中想要的近十年而且从来没有玩过。我认为这值得一试。你可能需要接受你必须杀死优雅铸造的梦想。

有一些内联的警告。您可能已经知道调试版本的提示被禁用(默认情况下)。您需要处于发布模式以分析/基准测试或修改构建设置。如果您尚未进入发布模式(或更改的构建设置),则可能会忽略您的内联提示。

请务必使用const来提示编译器进一步优化。即使它对你的情况不起作用,也是一个很好的做法。标记不应该改变的内容将防止各种灾难...并且还为编译器提供积极优化的机会。

伙计,我希望我知道德尔福现在是否允许跨单位内联,但我根本不这样做。许多C ++编译器只在同一源代码文件中内联,除非您将代码放在标头中(标头在Pascal中没有相关性)。值得一两次搜索。如果可以的话,尝试为其调用者本地化内联函数/方法。它至少会帮助编译时间,如果不是更多的话。

所有想法。希望这种蜿蜒有用。

答案 2 :(得分:0)

现在我想到了,也许绝对有必要将返回值放在不同的内存空间中并复制回分配给它的那个。

我正在考虑可能需要取消分配返回值的情况,比如调用一个接受带有Byte值的TAilmentP参数的函数......我认为你不能直接指定函数的参数,因为它还没有被创建,并且修复会破坏在汇编程序中生成函数调用的正常和已建立的方式(即:在创建之前尝试访问参数的字段是禁止的,所以你必须在此之前创建该参数,然后为其分配必须为OUTSIDE分配构造函数然后在汇编程序中调用该函数的方法。

对于其他运算符(您可以使用它来计算表达式,因此需要创建临时对象),这一点尤为明显,因为您认为它就像其他语言中的赋值运算符(如在C ++中,它可以是一个实例成员),但它实际上远不止于此 - 它也是一个构造函数。 例如

procedure ShowAilmentName (Ailment: TAilmentP);
begin
  ShowMessage (Ailment.Name);
end;

[...]
begin
ShowAilmentName (5);
end.

是的,隐式运算符也可以这样做,这很酷。 :d 在这种情况下,我认为5,就像任何其他字节一样,将被转换为TAilmentP(如在基于该字节创建新的TAilmentP对象时)给定隐式运算符,然后将对象按字节方式复制到Ailment参数,然后输入函数体,执行它的工作,并在返回时销毁从转换获得的临时TAilmentP对象。 如果Ailment是const,那就更加明显了,因为它必须是一个引用,并且也是常量的(在函数被调用后没有修改)。

在C ++中,赋值运算符与函数调用没有任何关系。相反,人们可以使用TAilmentP的构造函数来接受Byte参数。在Delphi中可以做同样的事情,我怀疑它优先于隐式运算符,但是C ++不支持但是Delphi做的是从运算符开始向下转换为原始类型(字节,整数等)使用类运算符重载。因此,像“程序ShowAilmentName(Number:Byte);”这样的过程。永远不会接受像C ++中的“ShowAilmentName(SomeAilment)”这样的调用,但在Delphi中它可以。

所以,我猜这是Implicit运算符的副作用,也就像构造函数一样,这是必要的,因为记录不能有原型(因此你不能只通过两个记录之间的单向转换和另一个转换)使用构造函数)。 其他人认为这可能是原因吗?