开发者

Delphi Interface Performance Issue

I have done some really serious refactoring of my text editor. Now there is much less code, and it is much easier to extend the component. I made rather heavy use of OO design, such as abstract classes and interfaces. However, I have noticed a few losses when it comes to performance. The issue is about reading a very large array of records. It is fast when everything happens inside the same object, but slow when done via an interface. I have made the tinyest program to illustrate the details:

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(IMyInterfac开发者_高级运维e, 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.

When I read the data directly, I get times like 0.14 seconds. But when I go through the interface, it takes 1.06 seconds.

Is there no way to achieve the same performance as before with this new design?

I should mention that I tried to set PArrayOfRecord = ^TArrayOfRecord and redefined IMyInterface.arr: PArrayOfRecord and wrote Arr^ etc in the for loop. This helped a lot; I then got 0.22 seconds. But it is still not good enough. And what makes it so slow to begin with?


Simply assign the array to a local variable before iterating through the elements.

What you're seeing is that the interface methods calls are virtual and have to be called through an indirection. Also, the code has to pass-through a "thunk" that fixes up the "Self" reference to now point to the object instance and not the interface instance.

By making only one virtual method call to get the dynamic array, you can eliminate that overhead from the loop. Now your loop can go through the array items without the extra overhead of the virtual interface method calls.


You're comparing oranges with apples, as the first test reads a field (FArr), while the second test reads a property (Arr) that has a getter assigned with it. Alas, interfaces offer no direct access to their fields, so you really can't do it any other way than like you did. But as Allen said, this causes a call to the getter method (GetArray), which is classified as 'virtual' without you even writing that because it's part of an interface. Thus, every access results in a VMT-lookup (indirected via the interface) and a method call. Also, the fact that you're using a dynamic array means that both the caller and the callee will do a lot of reference-counting (you can see this if you take a look at the generated assembly code).

All this is already enough reasons to explain the measured speed difference, but can indeed easily be overcome using a local variable and read the array only once. When you do that, the call to the getter (and all the ensueing reference counting) is taking place only once. Compared to the rest of the test, this 'overhead' becomes unmeasurable.

But note, that once you go this route, you'll loose encapsulation and any change to the contents of the array will NOT reflect back into the interface, as arrays have copy-on-write behaviour. Just a warning.


Patrick and Allen's answers are both perfectly correct.

However, since your question talks about improved OO design, I feel a particular change in your design that would also improve performance is appropriate to discuss.

Your code to set the Tag is "very controlling". What I mean by this is that you're spending a lot of time "poking around inside another object" (via an interface) in order to calculate your Tag value. This is actually what exposed the "performance problem with interfaces".

Yes, you can simply deference the interface once to a local variable, and get a massive improvement in performance, but you'll still be poking around inside another object. One of the important goals in OO design is to not poke around where you don't belong. This actually violates the Law of Demeter.

Consider the following change which empowers the interface to do more work.

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;

Then inside TForm3.FormCreate, //APPROACH 3 becomes:

Tag := MyInterface.GetTagValue;

This will be as fast as Allen's suggestion, and will be a better design.

Yes, I'm fully aware you simply whipped up a quick example to illustrate the performance overhead of repeatedly looking something up via interface. But the point is that if you have code performing sub-optimally because of excessive accesses via interfaces - then you have a code smell that suggests you should consider moving the responsibility for certain work into a different class. In your example TForm3 was highly inappropriate considering everything required for the calculation belonged to TMyObject.


your design use huge memory. Optimize your interface.

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜