在 Delphi 中模拟虚拟方法

Simulating virtual methods in Delphi

提问人:Anton Duzenko 提问时间:11/15/2014 最后编辑:Anton Duzenko 更新时间:11/17/2014 访问量:803

问:

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

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

TestOn(TSimdCpu);
TestOn(TGenericCpu);

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

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

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

评论

0赞 JensG 11/15/2014
Re “performance”:请验证您的代码(一旦完成)确实比简单的虚拟方法调用更快。关于“无意义的复杂性”:你想如何称呼你在这里所做的事情,你想花多少时间在一个“只用于测试”的项目上?
0赞 Anton Duzenko 11/15/2014
我称之为“达到德尔菲的极限”。我不做“拖拉机编码”的任何时间都很好。

答:

3赞 David Heffernan 11/15/2014 #1

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

您需要添加一些间接内容。创建保存过程变量的记录。举例说明:

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

  TMyRecord = record
    AddFunc: TAddFunc;
  end;

然后声明记录的两个实例。一个填充了 SSE 函数,另一个填充了非 SSE 引用函数。

在这一点上,你有你需要的东西。您可以传递这些记录,并使用它们提供的间接来编写通用测试代码。

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

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


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

评论

0赞 Anton Duzenko 11/15/2014
泛型能帮上忙吗?
1赞 David Heffernan 11/15/2014
可悲的是没有。我不认为你这样做是正确的。我会有一个条件,可以在变体之间切换编译。我相信您需要删除运行时重定向。
0赞 Arnaud Bouchez 11/16/2014
这只是重新创建 VMT。因此,恕我直言,常规虚拟方法的速度优势仅适用于一次查找。如果将方法定义为 ,而不是 ,则类虚拟方法和 VMT-record-trick 将执行完全相同的操作。class procedureprocedure
0赞 David Heffernan 11/17/2014
@Arnaud确实如此。Asker 希望避免使用虚拟方法,但具有相同的功能。因此,您最终会重新实现虚拟方法也就不足为奇了。这不是一个好主意。我说了这一切,不是吗?我根本不会这样做。我会直接调用方法,而不间接调用方法。
0赞 Anton Duzenko 11/15/2014 #2

大卫·赫弗南(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;

评论

0赞 Rudy Velthuis 11/16/2014
实际上,他可以将每个类放在自己的 include 中,然后单独测试。IOW,简单的复制粘贴多态性。毕竟,这只是一个简单的测试。
0赞 Anton Duzenko 11/16/2014
当您可以在 uses 子句中更改单位名称时,为什么要打扰 include
0赞 David Heffernan 11/17/2014
@Anton 吟诵单位名称正是我会做的。我会使用单位别名。
1赞 Arnaud Bouchez 11/16/2014 #3

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

如果你看一下 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 procedureprocedure

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

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

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

如果需要浮点性能,还可以考虑使用具有优化和优化 JIT 的语言。例如,您可以使用我们的 Delphi 开源 SpiderMonkey 库。你可以用纯 JavaScript 编写代码,然后让优化的 JIT 完成它的工作。您可能会对生成的速度感到惊讶:对于浮点,结果通常比 Delphi x87 原生代码更快。您将获得大量的开发时间。

评论

0赞 David Heffernan 11/17/2014
将任何东西与古老的 32 位编译器进行比较有点不公平。与现代的、现在成熟的 64 位编译器进行比较怎么样?
0赞 Anton Duzenko 11/17/2014
为什么不实际测试虚拟函数调用的速度。你对“一两次查找”的假设是错误的。我的代码在这里。那里没有什么可怀疑的。
0赞 Arnaud Bouchez 11/17/2014
我检查了asm。您是否尝试过使用类方法?它与记录中的函数指针相同。你没有提供任何代码,我认为这是不公平的。
0赞 David Heffernan 11/17/2014
@ArnaudBouchez 目前尚不清楚该评论是针对谁的