Delphi接口性能问题

23

我对我的文本编辑器进行了一些非常严格的重构。现在代码量大大减少,组件扩展也更加容易。我广泛使用了面向对象设计,如抽象类和接口。然而,当涉及到读取非常大的记录数组时,我注意到了一些性能上的损失。问题在于当所有操作都发生在同一个对象内部时速度很快,但是通过接口来完成时速度很慢。我制作了一个最简单的程序来说明细节:

unit Unit3;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

const
  N = 10000000;

type
  TRecord = record
    Val1, Val2, Val3, Val4: integer;
  end;

  TArrayOfRecord = array of TRecord;

  IMyInterface = interface
  ['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
    function GetArray: TArrayOfRecord;
    property Arr: TArrayOfRecord read GetArray;
  end;

  TMyObject = class(TComponent, IMyInterface)
  protected
    FArr: TArrayOfRecord;
  public
    procedure InitArr;
    function GetArray: TArrayOfRecord;
  end;

  TForm3 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form3: TForm3;
  MyObject: TMyObject;

implementation

{$R *.dfm}

procedure TForm3.FormCreate(Sender: TObject);
var
  i: Integer;
  v1, v2, f: Int64;
  MyInterface: IMyInterface;
begin

  MyObject := TMyObject.Create(Self);

  try
    MyObject.InitArr;

    if not MyObject.GetInterface(IMyInterface, MyInterface) then
      raise Exception.Create('Note to self: Typo in the code');

    QueryPerformanceCounter(v1);

    // APPROACH 1: NO INTERFACE (FAST!)
  //  for i := 0 to high(MyObject.FArr) do
  //    if (MyObject.FArr[i].Val1 < MyObject.FArr[i].Val2) or
  //         (MyObject.FArr[i].Val3 < MyObject.FArr[i].Val4) then
  //      Tag := MyObject.FArr[i].Val1 + MyObject.FArr[i].Val2 - MyObject.FArr[i].Val3
  //               + MyObject.FArr[i].Val4;
    // END OF APPROACH 1


    // APPROACH 2: WITH INTERFACE (SLOW!)    
    for i := 0 to high(MyInterface.Arr) do
      if (MyInterface.Arr[i].Val1 < MyInterface.Arr[i].Val2) or
           (MyInterface.Arr[i].Val3 < MyInterface.Arr[i].Val4) then
        Tag := MyInterface.Arr[i].Val1 + MyInterface.Arr[i].Val2 - MyInterface.Arr[i].Val3
                 + MyInterface.Arr[i].Val4;
    // END OF APPROACH 2

    QueryPerformanceCounter(v2);
    QueryPerformanceFrequency(f);
    ShowMessage(FloatToStr((v2-v1) / f));

  finally

    MyInterface := nil;
    MyObject.Free;

  end;


end;

{ TMyObject }

function TMyObject.GetArray: TArrayOfRecord;
begin
  result := FArr;
end;

procedure TMyObject.InitArr;
var
  i: Integer;
begin
  SetLength(FArr, N);
  for i := 0 to N - 1 do
    with FArr[i] do
    begin
      Val1 := Random(high(integer));
      Val2 := Random(high(integer));
      Val3 := Random(high(integer));
      Val4 := Random(high(integer));
    end;
end;

end.

直接读取数据时,我得到的时间是0.14秒。但是通过接口获取数据需要1.06秒。

有没有办法在这种新设计中实现与以前相同的性能?

值得一提的是,我尝试设置 PArrayOfRecord = ^TArrayOfRecord,重新定义了IMyInterface.arr: PArrayOfRecord并在for循环中使用了Arr^等操作。这确实有所帮助,时间缩短到了0.22秒。但仍然不够好。而且是什么让它变得如此缓慢呢?


2
我知道这只是一个快速拼凑的测试程序,但请先将MyInterface设置为nil,然后释放MyObject,否则会在已释放的对象上调用_Release。并且使用try..finally语句块。这样你就不会给新手树立错误的榜样了。 - The_Fox
4个回答

28

在遍历元素之前,只需将数组分配给一个本地变量。

你看到的是接口方法调用是虚拟的,并且必须通过间接方式调用。此外,代码必须经过一个“thunk”(一种固定了“Self”引用指向对象实例而不是接口实例的东西)。

通过仅进行一次虚拟方法调用来获取动态数组,您可以消除循环中的额外开销。现在,您的循环可以遍历数组项,而不会有额外的虚拟接口方法调用开销。


7
你现在在比较苹果和橘子,因为第一个测试读取了一个字段(FArr),而第二个测试读取了一个具有getter的属性(Arr)。遗憾的是,接口没有直接访问它们的字段的方法,所以你真的只能像你所做的那样进行操作。
但正如Allen所说,这会导致调用getter方法(GetArray),即使你没有编写它也被归类为“虚拟”的方法,因为它是接口的一部分。
因此,每次访问都会导致VMT查找(通过接口间接引用)和方法调用。
此外,使用动态数组意味着调用者和被调用者都将执行大量的引用计数(如果查看生成的汇编代码,可以看到这一点)。
所有这些已经足够解释速度差异,但确实可以轻松地通过使用本地变量并仅读取一次数组来克服。当你这样做时,对getter的调用(以及所有随后的引用计数)只会发生一次。与测试的其余部分相比,这种“开销”变得不可测量。
但请注意,一旦你走这条路,你将失去封装性,并且对数组内容的任何更改都不会反映到接口中,因为数组具有写入副本的行为。只是一个警告。

谢谢您的解释。(我只是在读取数组。) - Andreas Rejbrand

3

PatrickAllen 的答案都是完全正确的。

然而,既然您的问题涉及到改进 OO 设计,我认为讨论一种可以提高性能的特定设计更加合适。

您设置标签的代码“非常控制”。我的意思是,您花了很多时间“在另一个对象内部查找”(通过接口)以计算标签值。这实际上就是暴露了“接口性能问题”。

是的,您可以简单地将接口解除引用并赋值给本地变量,从而大大提高性能,但仍然会在另一个对象内部进行查找。OO 设计中的一个重要目标是不要在不属于自己的地方进行查找。这实际上违反了Demeter 法则

考虑以下更改,它使接口能够做更多的工作。

IMyInterface = interface
['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
  function GetArray: TArrayOfRecord;
  function GetTagValue: Integer; //<-- Add and implement this
  property Arr: TArrayOfRecord read GetArray;
end;

function TMyObject.GetTagValue: Integer;
var
  I: Integer;
begin
  for i := 0 to High(FArr) do
    if (FArr[i].Val1 < FArr[i].Val2) or
       (FArr[i].Val3 < FArr[i].Val4) then
    begin
      Result := FArr[i].Val1 + FArr[i].Val2 - 
                FArr[i].Val3 + FArr[i].Val4;
    end;
end;

然后在 TForm3.FormCreate 内部,//APPROACH 3 变为:

Tag := MyInterface.GetTagValue;

这将像 Allen 的建议一样快,并且会有更好的设计。
是的,我完全意识到你只是匆忙地举了一个例子来说明通过接口重复查找某些内容的性能开销。但是问题在于,如果您的代码因过多访问接口而表现不佳,则存在代码异味,表明您应考虑将某些工作的责任移至不同的类中。在您的示例中,考虑到计算所需的所有内容都属于 TMyObject,因此 TForm3 是高度不适当的。

1
你的设计使用了大量的内存。优化一下你的界面。
IMyInterface = interface
  ['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
    function GetCount:Integer:
    function GetRecord(const Index:Integer):TRecord;   
    property Record[Index:Integer]:TRecord read GetRecord;
  end;

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