3

我试图找到一种优雅的方法来访问我的程序的其他部分中的某些对象的字段,方法是使用存储字节的记录并通过使用同名的函数访问另一条记录的字段作为记录的字段。

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 字段要慢。

有没有办法让我的程序通过使用任何方法/函数将值直接分配给我的字段?即使内联似乎也不起作用。有没有办法返回对(记录)变量的引用,而不是对象本身?最后(很抱歉有点跑题了),为什么运算符重载是通过静态函数完成的?由于您可以访问对象字段而无需取消引用它们,因此使它们成为实例方法不会使其更快吗?这在这里和我的代码的其他部分真的会派上用场。

[编辑] 这是隐式运算符的汇编代码,具有所有优化且没有调试功能(甚至不是断点的“调试信息”)。

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 哦,是的,我的隐式运算符也没有内联。

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

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

4

3 回答 3

3

适合寄存器的返回类型——甚至记录——通过寄存器返回。只有较大的类型在内部转换为通过引用传递给函数的“输出”参数。

您的记录的大小为 1。复制您的记录与复制普通的Byte.

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

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


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

关于其他语言的优化,我要说的只是您应该查找名为 return-value optimization的术语,或其缩写 NRVO。之前在 Stack Overflow 上已经介绍过了。它与内联无关。

于 2010-01-19T18:41:37.913 回答
1

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

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

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

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

请务必使用 const 提示编译器进一步优化。即使它不适合您的情况,也是一个很好的练习。标记不应该更改的内容将防止各种灾难......并且另外让编译器有机会积极优化。

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

都没有想法。希望这种曲折有帮助。

于 2010-01-19T02:15:00.913 回答
0

现在我考虑一下,也许绝对有必要将返回值放在不同的内存空间中并复制回被分配的那个。

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

这对于其他运算符(您可以使用它评估表达式并因此需要创建临时对象)尤其正确和明显,只是不是这个,因为您认为它就像其他语言中的赋值运算符(如在 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 支持的是向下转换为原始类型(字节、整数等),因为运算符使用类运算符重载。因此,类似“procedure ShowAilmentName (Number: Byte);”的过程 在 C++ 中永远无法接受像“ShowAilmentName (SomeAilment)”这样的调用,但在 Delphi 中可以。

所以,我想这是隐式运算符也像构造函数的副作用,这是必要的,因为记录不能有原型(因此你不能通过仅使用构造函数在两条记录之间转换两种方式) . 其他人认为这可能是原因吗?

于 2010-01-19T17:11:48.430 回答