Проблемы с производительностью интерфейса Delphi - PullRequest
24 голосов
/ 18 октября 2010

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

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 секунды. Но это все еще не достаточно хорошо. И с чего это началось так медленно?

Ответы [ 4 ]

28 голосов
/ 18 октября 2010

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

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

7 голосов
/ 19 октября 2010

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

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

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

2 голосов
/ 30 июня 2012

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

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

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

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

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

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.

1 голос
/ 04 апреля 2012

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

IMyInterface = interface
  ['{C0070757-2376-4A5B-AA4D-CA7EB058501A}']
    function GetCount:Integer:
    function GetRecord(const Index:Integer):TRecord;   
    property Record[Index:Integer]:TRecord read GetRecord;
  end;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...