2010-10-18 113 views
23

我對文本編輯器做了一些非常認真的重構。現在代碼少得多,並且擴展組件更容易。我對OO設計做了相當多的使用,比如抽象類和接口。但是,在性能方面,我注意到了一些損失。這個問題是關於閱讀一大堆記錄。當一切發生在同一個對象內部時,速度很快,但通過接口完成時速度很慢。我已經做了tinyest程序來說明細節:Delphi界面性能問題

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爲零,然後釋放MyObject,否則在已釋放的對象上調用_Release。並試用..最後。這樣你就不會爲新手設置一個錯誤的例子。 – 2010-10-19 09:15:31

+1

@The_Fox:完成。 – 2010-10-22 15:39:17

回答

26

只需在之間迭代元素之前將該數組分配給局部變量即可。

你所看到的是接口方法調用是虛擬的,必須通過間接調用。此外,代碼必須通過修復「自我」引用的「thunk」,現在指向對象實例而不是接口實例。

通過只進行一次虛擬方法調用來獲取動態數組,您可以消除循環中的開銷。現在,您的循環可以通過數組項目,而無需虛擬接口方法調用的額外開銷。

+0

哦,是的!這似乎工作。非常感謝你。 – 2010-10-18 19:54:08

+0

謝謝,我剛剛學到了一個新的優化技巧。 – 2018-02-20 10:51:24

6

你比較桔子和蘋果,作爲第一個測試讀取場(法爾),而第二次測試讀取具有與其分配獲取方法的屬性(ARR)。唉,接口不提供直接訪問他們的領域,所以你真的不能做任何其他方式,而不是像你一樣。 但是,正如艾倫所說,這會導致對getter方法(GetArray)的調用,該方法被歸類爲「虛擬」,因此您甚至可以編寫它,因爲它是接口的一部分。 因此,每次訪問都會導致VMT查找(通過接口進行中斷)和方法調用。另外,你使用動態數組的事實意味着調用者和被調用者都會做很多引用計數(如果你看看生成的彙編代碼,你可以看到這一點)。

這一切已經足夠的理由來解釋所測得的速度差,但確實可以很容易地使用本地變量加以克服和讀取陣列只有一次。當你這樣做時,調用getter(以及所有隨後的引用計數)只發生一次。與其他測試相比,這種「開銷」變得無法衡量。

但要注意,一旦你走這條路,你會失去封裝及陣列的內容有任何改變將不會反映回接口,陣列具有寫入時複製行爲。只是一個警告。

+0

謝謝你的解釋。 (而我只是讀數組。) – 2010-10-19 12:48:39

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; 
2

PatrickAllen's答案都是完全正確的。

但是,由於您的問題是關於改進的面向對象設計的,所以我覺得您的設計中的一個特殊變化也會提高性能,因此需要進行討論。

您設置標籤的代碼是「非常控制」的。我的意思是,你花了很多時間「在另一個對象內部徘徊」(通過界面)來計算你的Tag值。這實際上暴露了「接口性能問題」。

是的,你可以簡單地將接口尊重一個本地變量,並獲得性能的巨大改進,但你仍然會在另一個對象內部探索。面向對象設計的重要目標之一是而不是徘徊在你不屬於的地方。這實際上違反了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