2010-10-18 13 views
23

He hecho algunas refactorizaciones realmente serias de mi editor de texto. Ahora hay mucho menos código, y es mucho más fácil extender el componente. Hice un uso bastante fuerte del diseño OO, como las clases abstractas y las interfaces. Sin embargo, he notado algunas pérdidas en lo que respecta al rendimiento. El problema es leer una gran variedad de registros. Es rápido cuando todo sucede dentro del mismo objeto, pero lento cuando se realiza a través de una interfaz. He hecho el programa tinyest para ilustrar los detalles:Delphi Interface Performance Issue

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. 

Cuando leí los datos directamente, consigo momentos como 0,14 segundos. Pero cuando paso por la interfaz, demora 1,06 segundos.

¿No hay forma de lograr el mismo rendimiento que antes con este nuevo diseño?

Debo mencionar que he tratado de establecer PArrayOfRecord = ^TArrayOfRecord y redefinió IMyInterface.arr: PArrayOfRecord y escribí Arr^ etc en el bucle for. Esto ayudó mucho; Luego obtuve 0.22 segundos. Pero aún no es lo suficientemente bueno. ¿Y qué lo hace tan lento para empezar?

+2

Sé que esto es solo un programa de prueba creado muy rápido, pero primero configure MyInterface en nil y luego libere MyObject; de lo contrario, se llama a _Release en un objeto liberado. Y usa una prueba ... por fin. Solo para que no establezcas un ejemplo incorrecto para los novatos. –

+1

@The_Fox: listo. –

Respuesta

26

Simplemente asigne la matriz a una variable local antes de iterando a través de los elementos.

Lo que está viendo es que las llamadas a métodos de interfaz son virtuales y tienen que ser llamadas a través de un direccionamiento indirecto. Además, el código tiene que pasar a través de un "thunk" que arregla la referencia "Self" para apuntar ahora a la instancia del objeto y no a la instancia de la interfaz.

Al hacer solo una llamada de método virtual para obtener la matriz dinámica, puede eliminar esa sobrecarga del bucle. Ahora su ciclo puede recorrer los elementos de la matriz sin la sobrecarga adicional de las llamadas al método de interfaz virtual.

+0

¡Oh, sí! Esto parece funcionar Muchas gracias. –

+0

Gracias, acabo de aprender un nuevo truco de optimización. –

6

Está comparando naranjas con manzanas, ya que la primera prueba lee un campo (FArr), mientras que la segunda prueba lee una propiedad (Arr) que tiene un getter asignado. Por desgracia, las interfaces no ofrecen acceso directo a sus campos, por lo que realmente no puede hacerlo de otra manera que como lo hizo. Pero, como dijo Allen, esto causa una llamada al método getter (GetArray), que se clasifica como 'virtual' sin que siquiera lo escriba porque es parte de una interfaz. Por lo tanto, cada acceso da como resultado una búsqueda VMT (indirecta a través de la interfaz) y una llamada a un método. Además, el hecho de que esté utilizando una matriz dinámica significa que tanto la persona que llama como la persona que llama harán una gran cantidad de recuento de referencias (puede ver esto si observa el código ensamblador generado).

Todo esto ya es suficiente para explicar la diferencia de velocidad medida, pero puede ser fácilmente superado con una variable local y leer la matriz una sola vez. Cuando haces eso, la llamada al captador (y todo el recuento de referencia que sigue) se lleva a cabo una sola vez. Comparado con el resto de la prueba, este 'sobrecarga' se vuelve inconmensurable.

Pero tenga en cuenta que una vez que realice esta ruta, perderá la encapsulación y cualquier cambio en el contenido de la matriz NO se reflejará en la interfaz, ya que las matrices tienen un comportamiento de copiado por escritura. Sólo una advertencia.

+0

Gracias por su explicación. (Y solo estoy leyendo el conjunto) –

1

su diseño usa una gran cantidad de memoria. Optimiza tu interfaz.

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 y Allen's respuestas son ambos perfectamente correcta.

Sin embargo, dado que su pregunta se refiere al diseño mejorado de OO, creo que un cambio particular en su diseño que también mejoraría el rendimiento es apropiado para analizar.

Su código para establecer la etiqueta es "muy controlador". Lo que quiero decir con esto es que pasas mucho tiempo "husmeando dentro de otro objeto" (a través de una interfaz) para calcular tu valor de Etiqueta. Esto es en realidad lo que expuso el "problema de rendimiento con las interfaces".

Sí, simplemente puede deferencia la interfaz una vez a una variable local, y obtener una mejora masiva en el rendimiento, pero todavía estará hurgando dentro de otro objeto. Uno de los objetivos importantes en el diseño OO es no meterse donde no pertenezca. Esto realmente infringe el Law of Demeter.

Considere el siguiente cambio que permite que la interfaz haga más trabajo.

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; 

A continuación, en el interior TForm3.FormCreate, // ENFOQUE 3 se convierte en:

Tag := MyInterface.GetTagValue; 

Ésta será tan rápido como la sugerencia de Allen, y habrá un mejor diseño.

Sí, soy plenamente consciente de que simplemente preparó un ejemplo rápido para ilustrar la sobrecarga de rendimiento al buscar repetidamente algo a través de la interfaz. Pero el punto es que si tiene un código que funciona de manera subóptima debido a accesos excesivos a través de interfaces, entonces tiene un olor a código que sugiere que debería considerar trasladar la responsabilidad de cierto trabajo a una clase diferente. En su ejemplo, TForm3 fue muy inapropiado considerando todo lo que requirió para el cálculo pertenecía a TMyObject.