Problema de Rendimiento de la Interfaz Delphi


He hecho una refactorización muy seria 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 intensivo del diseño OO, como clases abstractas e interfaces. Sin embargo, he notado algunas pérdidas cuando se trata de rendimiento. La cuestión es leer una gran variedad de registros. Es rápido cuando todo sucede dentro del mismo objeto, pero lento cuando se hace a través de una interfaz. He hecho el programa más pequeño para ilustrar el detalles:

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 leo los datos directamente, obtengo tiempos como 0.14 segundos. Pero cuando voy a través de la interfaz, se tarda 1,06 segundos.

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

Debo mencionar que traté de establecer PArrayOfRecord = ^TArrayOfRecord y redefinir IMyInterface.arr: PArrayOfRecord y escribí Arr^ etc en el bucle for. Esto ayudó mucho; luego obtuve 0.22 segundos. Pero todavía no es suficiente. ¿Y qué lo hace tan lento para empezar?

Author: Andreas Rejbrand, 2010-10-18

4 answers

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

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

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

 26
Author: Allen Bauer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-18 19:42:42

Estás 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 con ella. 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 siquiera escribirlo porque es parte de una interfaz. Por lo tanto, cada acceso resulta en una búsqueda VMT (indirectamente a través de la interfaz) y una llamada al método. Además, el hecho de que esté utilizando una matriz dinámica significa que tanto el llamante como el destinatario harán mucho recuento de referencias (puede ver esto si echa un vistazo al código de ensamblado generado).

Todo esto ya es razones suficientes para explicar la diferencia de velocidad medida, pero de hecho se puede superar fácilmente usando una variable local y leer el array solo una vez. Cuando haces eso, la llamada al getter (y toda la referencia de seguimiento contando) se está llevando a cabo solo una vez. En comparación con el resto de la prueba, esta 'sobrecarga' se vuelve inconmensurable.

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

 6
Author: PatrickvL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-19 06:09:38

Las respuestas de Patrick y de Allen son ambas perfectamente correctas.

Sin embargo, dado que su pregunta habla sobre el diseño mejorado de OO, creo que es apropiado discutir un cambio particular en su diseño que también mejoraría el rendimiento.

El código para establecer la etiqueta es "muy controlador". Lo que quiero decir con esto es que estás pasando mucho tiempo "husmeando dentro de otro objeto" (a través de una interfaz) para calcular el valor de tu etiqueta. Esto es realmente lo que exposed the"performance problem with interfaces".

Sí, simplemente puede deferir la interfaz una vez a una variable local y obtener una mejora masiva en el rendimiento, pero todavía estará husmeando dentro de otro objeto. Uno de los objetivos importantes en el diseño OO es no husmear donde no perteneces. Esto en realidad viola la Ley de Deméter .

Considere el siguiente cambio que faculta a la interfaz para hacer 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;

Luego dentro de TForm3.FormCreate, / / LA APROXIMACIÓN 3 se convierte en:

Tag := MyInterface.GetTagValue;

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

Sí, soy plenamente consciente de que simplemente armó un ejemplo rápido para ilustrar la sobrecarga de rendimiento de buscar repetidamente algo a través de la interfaz. Pero el punto es que si el código tiene un rendimiento subóptimo debido a accesos excesivos a través de interfaces, entonces tiene un olor a código que sugiere que debe considerar mover la responsabilidad de cierto trabajo en una clase diferente. En su ejemplo TForm3fue altamente inapropiado considerando todo requerido para el cálculo pertenecía a TMyObject.

 2
Author: Disillusioned,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:33

Su diseño utiliza una memoria enorme. 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;
 1
Author: MajidTaheri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-04 05:00:39