代码之家  ›  专栏  ›  技术社区  ›  Andreas Rejbrand

Delphi接口性能问题

  •  24
  • Andreas Rejbrand  · 技术社区  · 14 年前

    我对我的文本编辑器做了一些非常认真的重构。现在代码更少了,扩展组件也更容易了。我大量使用了OO设计,比如抽象类和接口。然而,我注意到在性能方面有一些损失。问题是读取大量的记录。当所有事情都发生在同一个对象中时,速度很快,但是当通过接口完成时,速度很慢。我已经做了一个最简单的程序来说明细节:

    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 写下了 Arr^ 等在 for 循环。这帮了我很大的忙,然后我得到了0.22秒。但还是不够好。又是什么让开始这么慢?

    4 回复  |  直到 14 年前
        1
  •  28
  •   Thomas Ahle    14 年前

    只需将数组赋给一个局部变量 之前 遍历元素。

    通过只调用一个虚拟方法来获取动态数组,可以从循环中消除该开销。现在,循环可以遍历数组项,而无需虚拟接口方法调用的额外开销。

        2
  •  7
  •   PatrickvL    14 年前

    但正如Allen所说,这会导致对getter方法(GetArray)的调用,它被分类为“virtual”,甚至不需要编写它,因为它是接口的一部分。 另外,使用动态数组意味着调用者和被调用者都将进行大量的引用计数(如果查看生成的程序集代码,可以看到这一点)。

    所有这一切已经足够解释测量的速度差,但确实可以很容易地克服使用一个局部变量和读取数组只有一次。当您这样做时,对getter的调用(以及随后的所有引用计数)只发生一次。与其他测试相比,这个“开销”变得无法测量。

    但是请注意,一旦走上这条路,就会失去封装,对数组内容的任何更改都不会反映到接口中,因为数组具有写时复制的行为。只是个警告。

        3
  •  3
  •   Community CDub    8 年前

    Patrick Allen's 答案都是完全正确的。

    但是,由于您的问题涉及到改进的OO设计,我觉得您的设计中有一个特殊的更改,也可以改进性能,因此应该进行讨论。

    是的,您只需将接口一次遵从于一个局部变量,就可以获得性能上的巨大改进,但是您仍然可以在另一个对象中四处寻找。面向对象设计的一个重要目标是 去你不属于的地方找找看。这实际上违反了 Law of 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 ,//方法3变成:

    Tag := MyInterface.GetTagValue;
    

    这将与艾伦的建议一样快,将是一个更好的设计。

    是的,我完全知道您只是简单地创建了一个简单的示例来说明通过接口重复查找某些内容的性能开销。但问题是,如果由于通过接口的过度访问而导致代码性能低于最佳值,那么您就有了一种代码味道,这意味着您应该考虑将某些工作的责任转移到另一个类中。在你的例子中 TForm3 考虑到 一切 所属计算所必需的 TMyObject .

        4
  •  1
  •   MajidTaheri    13 年前

    IMyInterface = interface
      ['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
        function GetCount:Integer:
        function GetRecord(const Index:Integer):TRecord;   
        property Record[Index:Integer]:TRecord read GetRecord;
      end;