2010-10-18 4 views
23

Ich habe ein paar wirklich ernsthafte Refactoring von meinem Texteditor gemacht. Jetzt gibt es viel weniger Code und es ist viel einfacher, die Komponente zu erweitern. Ich habe den OO-Entwurf, wie zB abstrakte Klassen und Interfaces, ziemlich stark genutzt. Ich habe jedoch ein paar Verluste in Bezug auf die Leistung bemerkt. Das Problem besteht darin, eine sehr große Reihe von Datensätzen zu lesen. Es ist schnell, wenn alles innerhalb desselben Objekts geschieht, aber langsam, wenn es über eine Schnittstelle erfolgt. Ich habe das tinyest Programm zu veranschaulichen die Details gemacht:Delphi Interface Performance Ausgabe

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. 

Wenn ich die Daten direkt lesen, ich mal wie 0,14 Sekunden erhalten. Aber wenn ich durch die Schnittstelle gehe, dauert es 1,06 Sekunden.

Gibt es keine Möglichkeit, mit diesem neuen Design die gleiche Leistung wie zuvor zu erzielen?

Ich sollte erwähnen, dass ich versucht PArrayOfRecord = ^TArrayOfRecord zu setzen und neu definiert IMyInterface.arr: PArrayOfRecord und schrieb Arr^ usw. in der for Schleife. Das hat sehr geholfen; Ich habe dann 0,22 Sekunden bekommen. Aber es ist immer noch nicht gut genug. Und warum ist es so langsam?

+2

Ich weiß, das ist nur ein Testprogramm wirklich schnell zusammengeworfen, aber bitte zuerst MyInterface auf Null und dann frei MyObject, sonst _Release wird auf einem freigegebenen Objekt aufgerufen. Und benutze einen Versuch..finally. Nur damit Sie den Neulingen kein falsches Beispiel geben. –

+1

@The_Fox: Fertig. –

Antwort

26

Ordnen Sie das Array einfach einer lokalen Variablen zu, bevor Sie durch die Elemente durchlaufen.

Was Sie sehen, ist, dass die Aufrufe der Schnittstellenmethoden virtuell sind und über eine Indirection aufgerufen werden müssen. Außerdem muss der Code einen "Thunk" durchlaufen, der die "Self" -Referenz so fixiert, dass sie jetzt auf die Objektinstanz und nicht auf die Schnittstelleninstanz zeigt.

Indem Sie nur einen virtuellen Methodenaufruf zum Abrufen des dynamischen Arrays machen, können Sie diesen Overhead aus der Schleife eliminieren. Jetzt kann Ihre Schleife die Array-Elemente ohne den zusätzlichen Aufwand der Methodenaufrufe der virtuellen Schnittstelle durchlaufen.

+0

Oh, ja! Dies scheint zu funktionieren. Vielen Dank. –

+0

Danke, ich habe gerade einen neuen Optimierungstrick gelernt. –

6

Sie vergleichen Orangen mit Äpfeln, da der erste Test ein Feld (FArr) liest, während der zweite Test eine Eigenschaft (Arr) liest, der ein Getter zugeordnet ist. Leider bieten Schnittstellen keinen direkten Zugriff auf ihre Felder, so dass Sie es wirklich nicht anders machen können als Sie. Aber wie Allen sagte, verursacht dies einen Aufruf der Get-Methode (GetArray), die als "virtuell" klassifiziert wird, ohne dass Sie dies schreiben, weil es Teil einer Schnittstelle ist. Somit führt jeder Zugriff zu einem VMT-Lookup (über die Schnittstelle weitergeleitet) und einem Methodenaufruf. Die Tatsache, dass Sie ein dynamisches Array verwenden, bedeutet auch, dass sowohl der Aufrufer als auch der Aufgerufene viel Referenzzählung durchführen (Sie können dies sehen, wenn Sie sich den generierten Assemblercode ansehen).

All dies ist bereits genug Gründe, um die gemessene Geschwindigkeitsdifferenz zu erklären, aber kann in der Tat leicht mit einer lokalen Variablen überwunden werden und das Array nur einmal lesen. Wenn Sie das tun, wird der Aufruf an den Getter (und alle folgenden Referenzzählungen) nur einmal ausgeführt. Im Vergleich zum Rest des Tests wird dieser "Aufwand" nicht messbar.

Aber sobald Sie diese Route gehen, verlieren Sie Kapselung und jede Änderung des Inhalts des Arrays wird nicht zurück in die Schnittstelle widerspiegeln, da Arrays Copy-on-Write-Verhalten haben. Nur eine Warnung.

+0

Vielen Dank für Ihre Erklärung. (Und ich lese gerade das Array.) –

1

Ihr Design verwenden großen Speicher. Optimiere deine Oberfläche.

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

Patrick und Allen's Antworten sind beide vollkommen richtig.

Da jedoch Ihre Frage über verbesserte OO-Design spricht, fühle ich eine besondere Veränderung in Ihrem Design, die auch die Leistung verbessern würde ist angemessen zu diskutieren.

Ihr Code zum Setzen des Tags ist "sehr kontrollierend". Was ich damit meine, ist, dass Sie viel Zeit darauf verwenden, "innerhalb eines anderen Objekts herumzustochern" (über eine Schnittstelle), um Ihren Tag-Wert zu berechnen. Dies ist tatsächlich, was das "Leistungsproblem mit Schnittstellen" ausstellt.

Ja, Sie können die Schnittstelle einfach einmal auf eine lokale Variable abstufen und erhalten eine massive Verbesserung der Leistung, aber Sie werden immer noch in einem anderen Objekt herumstochern. Eines der wichtigen Ziele in OO Design ist nicht herumzustochern, wo Sie nicht gehören. Dies verletzt tatsächlich die Law of Demeter.

Betrachten Sie die folgende Änderung, die die Schnittstelle zu mehr Arbeit befähigt.

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; 

Dann innen TForm3.FormCreate, // ANSATZ 3 wird:

Tag := MyInterface.GetTagValue; 

Dies wird so schnell wie Alle Vorschlag sein und ein besseres Design sein.

Ja, ich bin mir bewusst, dass Sie einfach ein schnelles Beispiel erstellt haben, um den Leistungs-Overhead zu veranschaulichen, der wiederholt über die Schnittstelle nach etwas sucht. Aber der Punkt ist, dass Code, der aufgrund übermäßiger Zugriffe über Schnittstellen suboptimal funktioniert, einen Code-Geruch aufweist, der darauf hinweist, dass Sie die Verantwortung für bestimmte Arbeiten in eine andere Klasse verschieben sollten. In Ihrem Beispiel TForm3 war sehr unpassend unter Berücksichtigung alles erforderlich für die Berechnung gehörte TMyObject.

Verwandte Themen