Подтвердить что ты не робот

Проблема производительности интерфейса Delphi

Я сделал действительно серьезный рефакторинг моего текстового редактора. Теперь гораздо меньше кода, и гораздо проще расширить компонент. Я довольно сильно использовал дизайн OO, например, абстрактные классы и интерфейсы. Тем не менее, я заметил несколько потерь, когда дело доходит до производительности. Проблема заключается в чтении очень большого массива записей. Это быстро, когда все происходит внутри одного и того же объекта, но медленно, когда выполняется через интерфейс. Я сделал самую маленькую программу, чтобы проиллюстрировать детали:

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 и написал Arr^ и т.д. в цикле for. Это очень помогло; Затем я получил 0,22 секунды. Но это все еще недостаточно. И что заставляет его так медленно начинать?

4b9b3361

Ответ 1

Просто назначьте массив локальной переменной до, итерации по элементам.

Что вы видите, так это то, что вызовы методов интерфейса являются виртуальными и должны быть вызваны через косвенное. Кроме того, код должен пройти через "thunk", который фиксирует ссылку "Self", теперь указывает на экземпляр объекта, а не на экземпляр интерфейса.

Сделав только один вызов виртуального метода для получения динамического массива, вы можете исключить эту служебную нагрузку из цикла. Теперь ваш цикл может проходить через элементы массива без дополнительных накладных расходов на вызовы метода виртуального интерфейса.

Ответ 2

Вы сравниваете апельсины с яблоками, так как первый тест читает поле (FArr), а второй тест считывает свойство (Arr), у которого есть геттер, назначенный вместе с ним. Увы, интерфейсы не дают прямого доступа к своим полям, поэтому вы действительно не можете делать это иначе, как вы. Но, как сказал Аллен, это вызывает вызов метода getter (GetArray), который классифицируется как "виртуальный", даже если вы его не пишете, потому что это часть интерфейса. Таким образом, каждый доступ приводит к поиску VMT (косвенным через интерфейс) и вызову метода. Кроме того, тот факт, что вы используете динамический массив, означает, что как вызывающий, так и вызываемый вызовут много подсчетов ссылок (вы можете увидеть это, если взглянете на сгенерированный код сборки).

Все это уже является достаточным основанием для объяснения измеренной разницы в скорости, но ее можно легко преодолеть с помощью локальной переменной и прочитать массив только один раз. Когда вы это делаете, вызов геттера (и все последующие подсчеты ссылок) происходит только один раз. По сравнению с остальной частью теста эта "накладная" становится неизмеримой.

Но обратите внимание, что после того, как вы пройдете этот маршрут, вы потеряете инкапсуляцию, и любые изменения в содержимом массива НЕ отразятся обратно в интерфейсе, так как массивы имеют поведение при копировании на запись. Просто предупреждение.

Ответ 3

Ответы

Патрик и Аллена оба абсолютно правильны.

Однако, поскольку ваш вопрос говорит об улучшенном дизайне OO, я чувствую, что конкретное изменение в вашем дизайне, которое также улучшит производительность, подходит для обсуждения.

Ваш код для установки тега "очень контролируется". Что я имею в виду, так это то, что вы тратите много времени на "прокручивание внутри другого объекта" (через интерфейс), чтобы вычислить значение вашего тега. Это на самом деле то, что выявило "проблему производительности с интерфейсами".

Да, вы можете просто отнести интерфейс один раз к локальной переменной и получить значительное улучшение производительности, но вы все равно будете ковырять внутри другого объекта. Одной из важных целей в дизайне OO является не, ткнуть туда, где вы не принадлежите. Это фактически нарушает Закон Деметры.

Рассмотрим следующее изменение, которое позволяет интерфейсу работать больше.

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,//APPROACH 3 становится:

Tag := MyInterface.GetTagValue;

Это будет так же быстро, как предложение Аллена, и будет лучшим дизайном.

Да, я полностью понимаю, что вы просто взвесили быстрый пример, чтобы проиллюстрировать накладные расходы на производительность, многократно просматривая что-то через интерфейс. Но дело в том, что если у вас есть код, выполняющий субоптимально из-за чрезмерного доступа через интерфейсы, - тогда у вас есть запах кода, который предполагает, что вы должны подумать о переносе ответственности за определенную работу в другой класс. В вашем примере TForm3 было крайне неуместно, учитывая все, что требуется для вычисления, принадлежало TMyObject.

Ответ 4

ваш дизайн использует огромную память. Оптимизируйте свой интерфейс.

IMyInterface = interface
  ['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
    function GetCount:Integer:
    function GetRecord(const Index:Integer):TRecord;   
    property Record[Index:Integer]:TRecord read GetRecord;
  end;