Pull to refresh

Comments 39

Поинтересуюсь, слышал ли автор про memory mapped files?
Их можно использовать для решения задачи, которую решает ваш самодельный буферизованный наследник TStream.
Не во всех случаях, но в принципе можно.
И для записи тоже. С учетом того, что можно маппировать «окно», а не весь файл, та же экономия получается. Другое дело, что в дельфях, кмк, удобнее сделать надстройку над TStream, т.к. зачастую именно такой поток используется для работы с данными, т.е. все равно придется те же методы описывать.
Некоторые «продвинутые новички» умудряются реализовать даже вот такой «говнокод»

try
T := TObject.Create;
// работаем с Т
finally
T.Free;
end;


В самом помещении конструктора/псевдоконуструктора внутри try блока ничего криминального нет. Особенно если это try..except блок, т.к. может понадобиться подчистка за конструткором без освобождения памяти (если lifetime объектов контроллируется другим объектом).
Не, тут ошибка, разбираем:
1. T — локальная переменная, значит шанс на то что она будет равна NIL достаточно призрачный.
2. В конструкторе класса происходит исключение (к примеру это TFileStream создающийся по не существующему пути)
3. происходит выход на обработчик исключения, где не созданному и, что более важно, не обниленному объекту, вызывается Free — здравствуй Access Violation :)
Ну, естественно, указатель нилится предварительно.
Ага, вот если бы нилился — тогда никаких претензий и не было бы, но не нилят :)
Решал специфичную задачу буферизации, а так же регулярно использую похожие проксики к памяти в видео стримов

Вот мой говнокод этими ништячками: pastebin.com/7nm8DRFr
Ну тут ты конечно развернулся, узнаю подход к методе :)
Небольшое замечание: не стоит дублировать статью специфическую для конкретного языка так же в общий хаб «Программирование», на который подписаны все, кто интересуется общепрограмисткими темами. Те, кого интересует конкретные технологии/языки — уже подписаны на соответствующие узкоспециализированные хабы и по любому увидят вашу статью.
class function TObjectDestroyer.Create(AObj: TObject): IUnknown;
var
  P: PObjectDestroyer;
begin
  if AObj = nil then Exit(nil);
  GetMem(P, SizeOf(TObjectDestroyer));
  P^.FVTable := @VTable;
  P^.FRefCount := 0;
  P^.FObj := AObj;
  Result := IUnknown(P);
end;
Ну если вы пошли уж так дико велосипедить — зачем аллоцировать в куче? Сложите на стек все сразу и все. Это же дорогущий враппер.
TMyRecord = record
  VTable: Pointer;
  Obj: TObject;
  Intf: IUnknown;
end;

class function TObjectDestroyer.Create(AObj: TObject): TMyRecord;
begin
  if AObj = nil then Exit(nil);
  Result.VTable := VTable;
  Result.Obj := AObj;
  Result.Intf := IUnknown(@Result);
end;
Я выкинул счетчик ссылок, опираясь на то, что мы TMyRecord не будем никуда передавать. Соотвественно надо будет учесть в _Release методе, что нам не надо ничего освобождать. Ну и поскольку нам таки нужна корректная финализация, а интерфейс должен лежать на стеке — добавил интерфейсную ссылку в TMyRecord. Как-то так.
Не успел подправить, само собой строка if AObj = nil then Exit(nil); не имеет смысла, нужно дефолтную пустую структуру возвращать: if AObj = nil then Exit(EmptyMyRecord);
Так не получится, ибо объект, контролируемый TObjectDestroyer нужно передавать наружу, соответственно стек уплывет при завершении работы локальной процедуры, и будет большой такой бадабум.
Ну и по поводу накладных расходов с GetMem, там куча не используется, а работает FastMem от Пьера Ле Риче, достаточно шустрый менеджеер памяти, выделяющий сразу много страниц и по вызову GetMem, просто резервирующий нужный участок на уже выделенной ранее через VitualAlloc памяти.
Так Result итак снаружи. Это переменная лежащая на стеке одним коллом выше. А типизированные рекорды грамотно финализировать делфя умеет.
Что-то я не уверен что так заработает, но надо поэксперементировать.
Да и вообще — ваш подход крайне опасен. Рассмотрим случай:
procedure TestWriteBySharedPtr;
var
  F1, F2: TFileStream;
  ConstData: DWORD;
begin
  ConstData := $DEADBEEF;
  F1 := TFileStream.Create('data1.bin', fmCreate);
  F2 := TFileStream.Create('data2.bin', fmCreate);
  TObjectDestroyer.Create(F1); //полагаемся на то, то на стеке неявно будет создана переменная интерфейсного типа
  TObjectDestroyer.Create(F2); //снова полагаемся на это
  F1.WriteBuffer(ConstData, SizeOf(ConstData));
  F2.WriteBuffer(ConstData, SizeOf(ConstData));
  //тут делфя невяно создат блок финализации, в котором вызовет Release для эти двух переменных
end;

Но нет никакой гарантии, что этих переменных будет две! Ведь если бы разработчики компилятора чуть-чуть вправили мозги оптимизатору — то очевидно, что после первого вызова
TObjectDestroyer.Create(F1);
оптимизатор может сообразить, что временная переменная на стеке не используется, и реюзнуть её во втором вызове:
TObjectDestroyer.Create(F2);
Что произойдет при реюзе? _Release для первого FileStream-а, и уничтожение объекта F1, и как следствие AV на строке
F1.WriteBuffer(ConstData, SizeOf(ConstData));

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

TObjectDestroyer.Create(F1);


Ведь ссылок нет, значит можно сразу говорить IntfClear, однако идеологически это не верно, поэтому описанная выше ситуация крайне маловероятна :)
Не не не. Очистка в секции финализации вконце одна, чтобы не создавать на каждый «пук» SEH фрейм. Тут все как раз правильно. Но оптимизатор ведь может сделать под капотом так:
procedure TestWriteBySharedPtr;
var
  F1, F2: TFileStream;
  ConstData: DWORD;
  temp1, temp2: IUnknown;
begin
  Pointer(temp1) := nil;
  Pointer(temp2) := nil;
  try
    ConstData := $DEADBEEF;
    F1 := TFileStream.Create('data1.bin', fmCreate);
    F2 := TFileStream.Create('data2.bin', fmCreate);
    temp1 := TObjectDestroyer.Create(F1); //тут будет вызван _AddRef для temp1
    temp2 := TObjectDestroyer.Create(F2); //тут будет вызван _AddRef для temp2
    F1.WriteBuffer(ConstData, SizeOf(ConstData));
    F2.WriteBuffer(ConstData, SizeOf(ConstData));
    //тут делфя невяно создат блок финализации, в котором вызовет Release для эти двух переменных
  finally
    temp1._Release;
    temp2._Release;
    //не обнуляю переменные т.к. делфя под капотом этого не делает насколько мне известно
  end;
end;

А может быть и так:
procedure TestWriteBySharedPtr;
var
  F1, F2: TFileStream;
  ConstData: DWORD;
  temp1, temp2: IUnknown;
begin
  Pointer(temp1) := nil;
  try
    ConstData := $DEADBEEF;
    F1 := TFileStream.Create('data1.bin', fmCreate);
    F2 := TFileStream.Create('data2.bin', fmCreate);
    temp1 := TObjectDestroyer.Create(F1); //тут будет вызван _AddRef для temp1
    temp1 := TObjectDestroyer.Create(F2); //тут будет вызван для temp1 сначала _Release, затем _AddRef для уже нового значения
    F1.WriteBuffer(ConstData, SizeOf(ConstData));
    F2.WriteBuffer(ConstData, SizeOf(ConstData));
    //тут делфя невяно создат блок финализации, в котором вызовет Release для эти двух переменных
  finally
    temp1._Release;
    //не обнуляю переменные т.к. делфя под капотом этого не делает насколько мне известно
  end;
end;

Стек меньше, т.е. оставшиеся данные чуть оптимальнее лягут на регистры. Инициализация короче. То есть это не бессмысленная оптимизация.
А вот пример гарантированного выстрела себе в ногу:
procedure TestWriteBySharedPtr;
var
  MyObjects: array of TMyObject;
  I: Integer;
begin
  SetLength(MyObjects, 32);
  for I := 0 to Length(MyObjects) - 1 do
  begin
    MyObjects[I] := TMyObject.Create;
    TObjectDestroyer.Create(MyObjects[I]); //вот тут будет одна временная переменная на стеке!!!
                                           //и она будет каждую итерацию перезаписываться, уничтожая созданные на предыдущей итерации объекты
  end;
  
  for I := 0 to Length(MyObjects) - 1 do 
    MyObjects[I].DoSomething(); //и как результат на первой же итерации AV, либо хуже.
                                //Мемори менеджер может реюзнуть освобожденное пространство, 
                                //и объект MyObjects[0] будет ссылаться на одно и то же пространство с объектом MyObjects[2] например
end;
В общем явно опасная штука.
Ну выстрелить в ногу можно гораздо проще, но для этого программисту и голова нужна :)
С последним вариантом — согласен, но тут можно использовать уже TSharedPtr, например вот так.

procedure TestWriteBySharedPtr2;
var
  MyObjects: array of TSharedPtr<TObject>;
  I: Integer;
begin
  SetLength(MyObjects, 32);
  for I := 0 to Length(MyObjects) - 1 do
    MyObjects[I] := TSharedPtr.Create(TObject.Create);
  for I := 0 to Length(MyObjects) - 1 do
    Writeln(MyObjects[I].Value.ClassName);
end;
Но согласитесь, если вдруг оптимизатор научится реюзать стековые перменные (что по хорошему итак должно быть) — то проблемы будут очень большие.
Правда с учетом того, как развивается компилятор — переживать ближайшие пару лет о качественном оптимизаторе не стоит, что не может не огорчать.
Безусловно соглашусь, тут у меня нет никаких возражений, но опять-же тогда просто используем везде TSharedPtr :)
Ну да, такой подход сработает.
Только на мой личный взгляд — не удобно через Value пользоваться объектом. + Накладные расходы.
Я кстати сторонник немножно сомнительного варианта:
T1 := nil;
T2 := nil;
T3 := nil;
try
  T1 := TObject.Create;
  T2 := TObject.Create;
  T3 := TObject.Create;
  // работаем со всеми тремя экземплярами Т1/Т2/Т3
finally
  T3.Free;
  T2.Free;
  T1.Free;
end;
Может быть когда-нибудь разработчики делфи добавят инициализацию объектных переменных, чтобы наконец то можно было писать вот так:

try
  T1 := TObject.Create;
  T2 := TObject.Create;
  T3 := TObject.Create;
  // работаем со всеми тремя экземплярами Т1/Т2/Т3
finally
  T3.Free;
  T2.Free;
  T1.Free;
end;

В любом случае за статью спасибо. Было приятно увидеть, что у вас есть точно такие же TStream велосипеды как и у меня. Помоему такой велосипед есть у каждого более менее опытного Delphi программиста.

p.s. Я бы мог пожалуй поделится велосипедом своего аля TMemoryStream-а, реализущего IStream. Дело в том что TStreamAdapter не умеет в IStream.Clone :(. Я не знаю зачем нужен IStream без этого метода, ибо если использовать его — значит отдавать куда-то наружу, а внешний код в 90% случаев сразу же вызывает Clone, чтобы не портить оригинальный Seek своими вызовами.
Конечно делитесь своими велосипедами, интересно же посмотреть ход мыслей в других реализациях)
Ок, приду домой — постараюсь подготовить. Вот только не знаю куда его прикрепить. Для отдельной статьи — маловато. В комментарий — многовато. Быть может Rouse позволит добавится в конец его статьи?
А может быть и так:

Может. Но Barry Kelly говорил, что такое маловероятно.
Интересно, а можно ссылочку где он такое говорил? Я лично не вижу логики в этом. Подход явно не оптимален, и нормальные компиляторы реюзают неиспользуемые стековые переменные.
ух… Говорил он это в своем блоге, как раз когда автодестроеерер предлагал(c другой реализацией, конечно) Ссылку конкретную не дам, это наверно лет 5 назад было, а может и больше. Когда только delphi2009 с дженериками вышла…
Если вы про это: blog.barrkel.com/2008/11/somewhat-more-efficient-smart-pointers.html то это не совсем то.
Так, как предлагал rouse с TSharedPtr<> (то же самое что по ссылке у Barry Kelly) — оно безусловно работает. Мы же явно храним переменную на стеке, и используем её. Правда использовать сам объект приходится через TSharedPtr<>.Value.
Я же выше показал опасность неявных стековых переменных. Стековая переменная не хранится, мы только надеемся что компилятор её создаст, и она целая и невредимая доедет до секции финализации. Посмотрите внимательно мой пример.
кстати, есть очевидное место, где имеется реюз переменных для интерфейсов — это циклы
type
  TTest = class(TInterfacedObject)
  public
    destructor Destroy; override;
  end;

function CreateTest: IUnknown;
begin
  Result := TTest.Create;
end;

procedure TForm3.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  for I := 0 to 3 do CreateTest;
  ShowMessage('AfterLoop');
end; // последний инстанс TTest - будет тут уничтожен.

{ TTest }

destructor TTest.Destroy;
begin
  ShowMessage('TTest.Destroy');
  inherited;
end;
Я как бы об этом и говорил в третьем примере в комментарии, на который вы ответили. :)
>> Я выкинул счетчик ссылок, опираясь на то, что мы TMyRecord не будем никуда передавать
К сожалению дельфя может сама, неявно копировать рекорды. Можно попробовать реализовать что то типа плюсового auto_ptr'а конечно, но я бы не рисковал. Менеджер памяти в дельфе действительно хорош и дает не таких уж сильные накладные расходы.
Sign up to leave a comment.

Articles