2010-10-18 108 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