【问题标题】:Simulating virtual methods in Delphi在 Delphi 中模拟虚拟方法
【发布时间】:2014-11-15 08:39:30
【问题描述】:

我正在为 SSE 指令编写一个 Delphi 接口。这是一个类(为了可见性等)具有 N 个类方法的 TSimdCpu(每个 SSE 指令一个;明显的性能开销现在不是问题)。

现在我想将我的代码的性能(虽然很慢)与做同样事情的纯帕斯卡代码进行比较。我的第一个猜测是编写一个具有相同方法名称的类似类 TGenericCpu。但是如果没有通用的基类和虚拟方法,我就不能只拥有一段测试代码来调用它应该运行测试的任何类的方法。 理想情况下,我想要类似的东西

TestOn(TSimdCpu);
TestOn(TGenericCpu);

但是我不知道如何不使用使用delphi的虚拟方法来实现这个。 我不想回退到虚拟方法,原因有两个:一个是性能,另一个是它只用于测试,而对于所有实际用途,它会增加毫无意义的复杂性。

泛型在这里有用吗?类似的东西

TTest<T> = class
...
T.AddVector(v);
...
TTest<TSimdCpu>.Test;
TTest<TGenericCpu>.Test;

【问题讨论】:

  • Re "performance":请验证您的代码(一旦完成)确实比简单的虚拟方法调用更快。关于“无意义的复杂性”:您想如何称呼您在这里所做的事情以及您想为“仅用于测试"?
  • 我称之为“达到德尔福的极限”。我不做“拖拉机编码”的任何时间都很好。

标签: delphi


【解决方案1】:

您想要实现一些看起来像虚拟方法但出于性能原因不使用虚拟方法或接口的东西。

您需要添加一些间接性。创建一个包含过程变量的记录。举例说明:

type
  TAddFunc = function(a, b: Double): Double;

  TMyRecord = record
    AddFunc: TAddFunc;
  end;

然后声明记录的两个实例。一种填充 SSE 函数,另一种填充非 SSE 参考函数。

此时你已经拥有了你需要的东西。您可以传递这些记录并使用它们提供的间接性来编写通用测试代码。

不过,这种间接方式是有代价的。毕竟,您在这里拥有的是接口的手动实现。预计函数调用的性能开销与接口相似。

我希望,除非您的操作数是大型数组,否则间接成本会扭曲您的基准。我知道您特别询问如何使用间接实现测试,但我个人希望使用尽可能接近真实代码的测试。这意味着测试直接函数调用。


你问的是泛型。它们对你没有用。为了创建在被测类上参数化的泛型类,您需要被测类从公共基类派生,或实现公共接口。然后你又回到了你开始的地方。

【讨论】:

  • 泛型有什么帮助吗?
  • 遗憾的是没有。我不认为你这样做是正确的。我有一个在变体之间切换编译的条件。我相信您需要删除运行时重定向。
  • 这只是重新创建一个 VMT。因此,恕我直言,从常规虚拟方法中受益的速度仅适用于一次查找。如果您将方法定义为 class procedure 而不是 procedure,则类虚拟方法和 VMT-record-trick 的执行方式完全相同。
  • @Arnaud 确实如此。 Asker 希望避免使用虚拟方法,但具有相同的功能。因此,您最终会重新实现虚拟方法也就不足为奇了。不是一个好主意。我说这一切不是吗?我根本不会这样做。我会直接调用方法而无需间接。
【解决方案2】:

在您的代码中,主要的速度差异不会出现在函数调用之间。

如果你看一下 asm,虚拟方法调用类似于

mov eax,object
mov ebx,[eax]  // get the the class info VMT
call dword ptr [ebx+##] // where ## is the virtual method offset

而一个非虚方法是

mov eax,object
call SomeAbsoluteAddress

对于指向函数的指针(在堆栈上)

mov eax,object
call dword ptr [ebp+##] // where ## is the pointer in the stack

您只是在类信息 VMT 中获得一两次查找。

我怀疑您的测试针对指向函数的指针进行了过度优化,因为指针可能在堆栈上。在实际代码中,您必须将指针存储在某处,因此与虚拟方法调用相比,您将一无所获。

如果您将方法定义为class procedure 而不是procedure,我怀疑类虚拟方法和函数重定向的执行方式完全相同:

mov eax,classinfo
call dword ptr [eax+##] // where ## is the virtual method offset

对于这样的计算,真正加快进程的可能根本不是调用函数,而是创建某种简单的 JIT。在运行函数之前创建二进制操作码流,通过查看 asm 操作码,然后创建一个包含执行流的缓冲区,并直接执行它。在这里,我们将讨论性能。它类似于内联函数调用。

我知道至少有两个(最近和维护的)项目使用 Delphi 编写的这种 JIT 编译:Besen JavaScript engineDelphi Web Script。 Besen 复制 asm 存根来创建 JITted 缓冲区,而 DWS 通过一组生成器方法计算操作码。

如果您需要浮点性能,还可以考虑使用经过调整和优化的 JIT 语言。你可以使用例如我们的开源SpiderMonkey library for Delphi。您可以用纯 JavaScript 编写代码,然后让优化的 JIT 完成它的工作。您可能会对结果速度感到惊讶:结果是usually faster than Delphi x87 native code,表示浮点数。您将获得大量开发时间。

【讨论】:

  • 将任何东西与古老的 32 位编译器进行比较有点不公平。与现代的、现在成熟的 64 位编译器进行比较怎么样?
  • 你为什么不实际测试一下虚函数调用的速度。您对“一两次查找”的假设是错误的。我的代码在这里。没有什么值得怀疑的。
  • 我检查了汇编。您是否尝试使用类方法?它与记录中的函数指针相同。你没有提供任何代码,我认为这是不公平的。
  • @ArnaudBouchez 目前尚不清楚该评论是针对谁的
【解决方案3】:

David Heffernan 的想法似乎是目前唯一的方法。我做了一个快速测试 - 结果如下:

simd 516 ms (pointer to a function, asm)
JensG 1187 ms (virtual method, asm)
generic 2797 ms (pointer to a function, pascal)
generic virtual 3360 ms (virtual method, pascal)

普通函数调用和虚函数调用之间的差异对于pascal代码可能相对较小,但对于asm则不然

  if cpu = nil then
    if test.name = 'JensG' then
      for i := 1 to N do begin
        form1.JensGAdd(v1^);
        form1.JensGMul(v2^);
      end
    else
      for i := 1 to N do begin
        form1.GenericAdd(v1^);
        form1.GenericMul(v2^);
      end
  else
    for i := 1 to N do begin
      cpu.AddVector(v1^);
      cpu.MulVector(v2^);
    end;

【讨论】:

  • 其实他可以把每个类放在自己的include中,然后分别测试。 IOW,简单地复制和粘贴多态性。毕竟,这只是一个简单的测试。
  • 当您可以在使用子句中更改单位名称时,为什么还要打扰包含
  • @Anton Chaning 单位名称正是我会做的。我会使用单位别名。
猜你喜欢
  • 1970-01-01
  • 2011-09-25
  • 2012-07-29
  • 2013-10-23
  • 1970-01-01
  • 2010-11-07
  • 2013-05-01
  • 2011-11-28
  • 2016-09-28
相关资源
最近更新 更多