12 января 2009 г.

Менеджеры памяти в программах

Почти в каждом языке используется свой менеджер памяти. Зачем он нужен и какие есть особенности, если модули программы написаны на разных языках?

Примечание: чтобы разбираться с менеджерами памяти, хорошо бы иметь представление и уметь работать с указателями.

Если вы читаете эту статью при проектировании своей DLL, то лучше начните отсюда: Разработка API (контракта) для своей DLL.

Менеджер памяти в Delphi

Менеджер памяти - это код, несколько функций в программе, которые отвечают за управление памятью (выделение и освобождение). Все запросы программы к памяти (как-то: классы, динамические массивы, строки, нетипизированные указатели) обслуживаются менеджером памяти. В Delphi менеджер памяти представлен записью (*):
type
  // В старых Delphi
  TMemoryManager = record
    GetMem: function(Size: Integer): Pointer;
    FreeMem: function(P: Pointer): Integer;
    ReallocMem: function(P: Pointer; Size: Integer): Pointer;
  end;

  // В новых Delphi
  TMemoryManagerEx = record
    { Базовая (обязательная) функциональность }
    GetMem: function(Size: Integer): Pointer;
    FreeMem: function(P: Pointer): Integer;
    ReallocMem: function(P: Pointer; Size: Integer): Pointer;
    { Дополнительная (опциональная) функциональность }
    AllocMem: function(Size: Cardinal): Pointer;
    RegisterExpectedMemoryLeak: function(P: Pointer): Boolean;
    UnregisterExpectedMemoryLeak: function(P: Pointer): Boolean;
  end;

Как видите, менеджер памяти представлен функциями выделения и освобождения памяти, а также изменения размера блока памяти. Эта запись хранится во внутренней глобальной переменной MemoryManager в модуле System. А поменять её мы можем вызовами Get/SetMemoryManager.

Зачем нужен менеджер памяти?

Зачем вообще нужен менеджер памяти? Почему нельзя использовать функции управления памятью операционной системы?

Потому что это будет утечка абстракции. Системный способ выделить память - это функция VirtualAlloc. Проблема в том, что эта функция слишком близко сидит к железу: её гранулярность выделения памяти - 64 Кб. Это аналогично тому, как в файловой системе файлы адресуются только кластерами (размер кластера обычно варьируется от 4 Кб до 64 Кб).

Иными словами, нельзя выделить памяти меньше, чем 64 Кб. Т.е. если вы создаёте 100 объектов по, скажем, 12 байт (очень простые объекты, вы их наследовали от TObject), то вместо двух килобайт (12 б * 100 = 1.2 Кб + служебные данные менеджера памяти) вы занимаете уже почти 6.5 Мб (64 * 100 = 6'400 Кб) - на несколько порядков больше! А строки? В типичной программе используется несметное количество строк, размер которых обычно не превышает одного предложения (все эти Caption, Hint, MessageBox и т.п.). Использовали бы вы VirtualAlloc - вы бы очень быстро исчерпали свободную память (а ведь есть ещё проблема фрагментации памяти).

Как это решает менеджер памяти программы? Это легко понять, исходя из принципа его работы. Он выделяет себе несколько рабочих кусков в памяти с помощью VirtualAlloc (а также выделяет их в дальнейшем по мере необходимости). Блоки эти имеют достаточный размер и всегда кратны размерам страницы. Когда программа просит его выделить память, он "выделяет" её в своих блоках. "Выделяет" не зря взято в кавычки. Ведь на самом деле он просто возвращает указатель на какую-то часть одного из своих рабочих блоков памяти - ту, которую он считает свободной по своим записям. Тогда при выделении тех же 100 объектов по 12 байт они будут занимать, например, часть одного блока в 4 кб. Конечно, это будет уже не ровно 1.2 Кб, т.к. нужно же ещё где-то хранить информацию о том, что вот в этой части блока у нас что-то лежит, а вот в том - ещё нет (она свободна). В чём-то менеджер памяти программы аналогичен продвинутой файловой системе типа NTFS (например, NTFS может хранить несколько мелких файлов в одном кластере, другие системы могут складывать "хвосты" файлов в один кластер). Окей, это грубое описание, но для начала сойдёт и такое.

Иными словами, менеджер памяти программы сглаживает особенности/требования системного выделения памяти (часто его также оптимизируют для быстрого выполнения именно типичных операций в языке).

Отсутствие стандарта на менеджеры памяти

Из-за того, что в каждом языке используется менеджер памяти, да плюс отсутствие стандарта на таковые (это чисто внутренняя архитектура языка, никак не связанная с ОС или другими языками), то это и приводит к проблеме передачи данных между модулями (имеется ввиду между исполняемыми модулями - т.е. между DLL и exe, а не unit-ами). Менеджер памяти одного модуля ничего не знает про менеджер памяти в другом модуле, и попытка освободить память, выделенную не им, обычно приводит к плохим вещам (AV, например).

Например, вы из DLL вернули String:
function gg: String;
begin
  Result := 'dfgfdg' + IntToStr(SomeOtherFunc); // << в этой строке скрытый вызов GetMem
  // Поскольку мы сейчас в DLL, то вызов GetMem - это вызов GetMem менеджера памяти DLL
  // Иными словами, в строке выше менеджер памяти DLL выделит память для строки
end;

exports
  gg;
В exe вы её используете. Как только закончили использовать - вы её удаляете. Вы не знаете ничего про менеджер памяти в DLL, поэтому всё, что вы можете сделать со строкой, - это передать её своему менеджеру памяти:
function gg: String; external 'test.dll';
...
var
  S: String;
begin
  S := gg;
  ShowMessage(S);
end; // << здесь строка S будет освобождена; скрытый вызов FreeMem для памяти S 
// Запрос на освобождение получит менеджер памяти в exe, 
// поскольку сейчас мы находимся в exe
// Поскольку менеджер памяти в exe не выделял эту память (её выделил менеджер памяти в DLL),
// то будут происходить плохие вещи (исключения Invalid Pointer или Access Violation).

Решение проблем межмодульного обмена памятью

Обычным решением таких проблем является использование "волшебного" правила: "кто память выделил - тот её и освобождает". Одним из вариантов реализации является:

Использование общего менеджера памяти


Без общего менеджера памятиИспользование общего менеджера памяти
На обоих рисунках (очень грубо, без соблюдения масштаба, показаны далеко не все объекты и т.п.) изображены пользовательские части адресного пространства процесса (т.е. области от 0 до 2 Гб (**)). Слева приведён вариант без использования общего менеджера памяти - т.е. ситуация по-умолчанию. Справа - использование общего менеджера памяти в виде отдельной DLL (***).

В первом случае у нас кроме основной программы (exe) загружена ещё одна DLL. Всего в программе выделено три блока памяти (с точки зрения системы). Два из них выделил exe, а один - DLL. Связь этих блоков с менеджерами памяти изображается стрелочками справа. Синие стрелочки - это связи менеджера памяти в exe, а красные - менеджера в DLL. В каждом блоке скруглёнными прямоугольниками также изображены логические выделения памяти в модуле. Это строки, массивы, объекты (включая компоненты) и другие динамические данные в вашей программе. Это то, что ваш код в программе считает выделенной памятью.

Во втором случае и exe и DLL загружают DLL общего менеджера памяти и используют его. По сути, менеджеры памяти в обоих модулях просто перенаправляют свои вызовы на общий менеджер памяти (светло-зелёные стрелки слева), а уж он и занимается реальным выделением памяти (синие стрелки справа).

Вот рассмотрим пример, когда программа вызывает функцию из DLL, а та возвращает строку String. Без использования общего менеджера памяти строку String выделяет менеджер DLL библиотеки, поэтому информация о ней лежит на левом рисунке по красным линиям. После того, как в главном приложении вы обработали эту строку, она будет удалена автоматически, как только станет вам не нужна (ну или вы сами явно её не обнулите). Но освобождать-то строку будет уже код программы. Это значит, что он попытается найти запись о строке по синим стрелочкам (левый рисунок), но она-то лежит в одном из блоков, на который указывают красные стрелочки. Упс-с...

Что в этом же сценарии будет при использовании общего менеджера памяти? Строку String выделяет менеджер DLL библиотеки. Но поскольку используется общий менеджер памяти, то менеджер памяти библиотеки перенаправляет запрос общему менеджеру памяти (на правом рисунке зелёная стрелка), поэтому информация о строке лежит на правом рисунке по синим линиям. После того, как в главном приложении вы обработали эту строку, она будет удалена автоматически, как только станет вам не нужна (ну или вы сами явно её не обнулите). Но освобождать-то строку будет уже код программы. Поскольку используется общий менеджер памяти, то менеджер памяти программы перенаправит запрос об освобождении памяти общему менеджеру памяти (зелёная стрелка). Ну а тут уже нет никаких проблем: блок памяти общему менеджеру памяти доступен и известен, ибо это он же его и выделял. Всё чисто, проблем нет.

Часто, как наиболее простой вариант автоматического выполнения "волшебного" правила, советуют делать подключение (в uses) модуля ShareMem (или его аналога) первым модулем во всех проектах (и DLL и exe) - это и есть использование общего менеджера памяти. Оба условия - подключение первым модулем и подключение во всех проектах - являются необходимыми. Представьте себе такую ситуацию: в модуле была выделена строка (например), потом был установлен менеджер памяти ShareMem, затем модуль свою строку удаляет. Но запрос на удаление он посылает не старому менеджеру, а ShareMem! Или, наоборот: при установленном ShareMem была выделена память, а потом, при завершении работы, вы вернули на место старый менеджер, и память освобождается в старом менеджере, вместо ShareMem. Т.е. ситуация в каком-то смысле аналогична левой картинке, несмотря на использование общего менеджера памяти: в программе активно более одного менеджера памяти и правило "кто выделил - тот и освобождает" не выполняется. Ну а со всеми проектами ещё очевидней. Если у вас exe использует общий менеджер памяти, а DLL - нет (или наоборот), то снова получается несколько активных менеджеров памяти в программе => нарушение правила.

Стоит заметить, что иногда ваша программа может использовать общий менеджер памяти без специальных действий. Но только для особых типов данных. Например, простейший способ передать из DLL строку (без использования явного общего менеджера памяти) - использовать тип WideString. Дело в том, что WideString (в отличие от String, AnsiString и UnicodeString) является (на самом деле) системным типом BSTR. А у него есть обязательное требование: всё управление памяти должно выполняться через системный менеджер памяти. Это означает, что если вы выделили WideString в DLL, а потом освободили её в exe, то оба запроса (на выделение и освобождение памяти) получит один и тот же менеджер памяти - системный, а вовсе не дельфёвые (разные) менеджеры памяти. Таким образом, в вашей программе может использоваться разделяемый менеджер памяти (в этом примере в его качестве выступает системный менеджер), хотя специально вы ничего не делали.

Пример с WideString, DLL:
function DoSomething(const A: WideString): WideString; stdcall; 
begin
  Result := A + 'sdf' + A;
end;

exports
  DoSomething;
exe:
function DoSomething(const A: WideString): WideString; stdcall; external 'Project2.dll';

procedure TForm1.Button1Click(Sender: TObject);
begin
  Caption := DoSomething(IntToStr(5));
end;
Итак, общий менеджер памяти позволяет автоматически выполнять правило "кто выделил - тот и освобождает". Но это не единственный способ. При невозможности использования общего менеджера памяти (например, exe и DLL пишутся на разных языках) необходимо использовать некоторый свод правил, гарантирующих выполнения "волшебного" правила:

Ручное выполнение правила

Есть три основных варианта:
- вызываемый сообщает вызывающему, сколько памяти нужно выделить; память выделяет и освобождает вызывающий; это самый типичный способ при вызове функций WinAPI.
- вызываемый сам выделает память, а для её освобождения предоставляет отдельную функцию очистки; память выделяет и освобождает вызываемый.
- вызываемый и вызывающий просят третью сторону выполнять управление памятью; обычно этой третьей стороной выступает системный менеджер памяти; фактически, это вариант с общим (разделяемым) менеджером памяти, но только не глобально, а лишь для некоторых данных.

Конкретных реализаций обоих способов можно придумать много. Например, как вариант второго способа можно рассматривать и обычные объекты Delphi или интерфейсы. В любом объекте Delphi используется виртуальный деструктор. Это значит, что в объекте записана ссылка на код деструктора - это и есть "функция очистки". Удаление объекта - это вызов его деструктора. Ссылка на деструктор у объекта указывается. Код деструктора всегда располагается в том же модуле, что и код конструктора. Это значит, что если контруктор создал объект, то деструктор этого же модуля объект удалит. Именно в этом модуле, а не в каком либо ещё. Т.е. "волшебное" правило выполняется. Аналогичные рассуждения справедливы и для интерфейсов - в них роль деструктора играет вызов Release.

Именно благодаря такой схеме и работает, например, передача исключений из DLL в exe, если они написаны в одной версии Delphi, без подключения общего менеджера памяти. В DLL вызывается исключение, его объект передаётся в exe, exe после обработки исключения удаляет объект, что приводит к вызову деструктора, который расположен в DLL (ссылка на него лежала в самом объекте исключения). Всё чисто и гладко. Проблемы начнутся, когда вы начнёте менять свойства объекта исключения - например, захотите добавить в конце Message точку (on E: Exception do E.Message := E.Message + '.'). Присвоение свойству Message означает, что старая строка, которая там лежала, должна быть удалена. Но создавал-то её код в DLL, а попытается удалить код из exe. Поскольку ничего даже отдалённо похожего на виртуальный деструктор у строки нет, то, соответственно, и освобождать память будет код из exe. Выделили в DLL, освобождаем в exe - вот вам и AV.

Давайте для полноты картины дадим простейшие примеры каждого способа.

Способ 1, DLL:
function DoSomething(const A: PChar; const Data: PChar; var Len: Cardinal): Bool; stdcall;
var
  Rslt: String;
  Sz: Integer;
begin
  Rslt := A + 'sdf' + A;
  if (Data = nil) and (Len = 0) then
  begin
    Len := Length(Rslt);
    SetLastError(ERROR_INSUFFICIENT_BUFFER);
    Result := False; 
  end  
  else
  begin
    if Data = nil then
    begin
      SetLastError(ERROR_INVALID_PARAMETER);
      Result := False; 
      Exit;
    end;  

    Sz := Length(Rslt);
    if Len < Sz then
      Sz := Len;
    Sz := (Sz + 1) * SizeOf(Char);

    if Rslt = '' then
      Data^ := #0
    else
      Move(Pointer(Rslt)^, Data^, Sz);
  
    if Len < Length(Rslt) then
      SetLastError(ERROR_INSUFFICIENT_BUFFER);
    Result := True;
  end;
end;

exports
  DoSomething;
exe:
function DoSomething(const A: PChar; const Data: PChar; var Len: Cardinal): Bool; stdcall; external 'Project2.dll';

procedure TForm1.Button1Click(Sender: TObject);
var
  S: String;
  D: String;
  Len: Cardinal;
begin
  S := IntToStr(5);

  D := '';
  Len := 0;
  DoSomething(PChar(S), PChar(D), Len);
  SetLength(D, Len);
  if not DoSomething(PChar(S), PChar(D), Len) then
    RaiseLastOSError;
  SetLength(D, Len);

  Caption := D;
end;

Способ 2, DLL:
function DoSomething(const A: PChar): PChar; stdcall; 
var
  Rslt: String;
  Sz: Integer;
begin
  Rslt := A + 'sdf' + A;

  Sz := (Length(Rslt) + 1) * SizeOf(Char);
   
  GetMem(Result, Sz);
  Move(Pointer(Rslt)^, Result^, Sz);
end;

procedure FreeDoSomethignResult(const A: PChar); stdcall;
begin
  FreeMem(A);
end;

exports
  DoSomething,
  FreeDoSomethignResult;
exe:
function DoSomething(const A: PChar): PChar; stdcall; external 'Project2.dll';
procedure FreeDoSomethignResult(const A: PChar); stdcall; external 'Project2.dll';

procedure TForm1.Button1Click(Sender: TObject);
var
  S: PChar;
begin
  S := DoSomething(IntToStr(5));
  Caption := S;
  FreeDoSomethignResult(S);
end;

Способ 3, DLL:
function DoSomething(const A: PChar): PChar; stdcall; 
var
  Rslt: String;
  Sz: Integer;
begin
  Rslt := A + 'sdf' + A;

  Sz := (Length(Rslt) + 1) * SizeOf(Char);
   
  Result := Pointer(LocalAlloc(0, Sz));
  if Result <> nil then
    Move(Pointer(Rslt)^, Result^, Sz);
end;

exports
  DoSomething;
exe:
function DoSomething(const A: PChar): PChar; stdcall; external 'Project2.dll';

procedure TForm1.Button1Click(Sender: TObject);
var
  S: PChar;
begin
  S := DoSomething(IntToStr(5));
  Caption := S;
  LocalFree(Cardinal(S));
end;
Заметьте, что в примерах выше строковые данные используются только для примера. В реальном коде вместо PChar предпочтительнее использовать WideString (и тогда манипуляции с указателями и памятью будут не нужны), а методы примеров применять к другим динамическим данным (например, - массивам).

Ну и раз уж я упомянул интерфейсы, то давайте покажу код и с ними - на этот раз с динамическим массивом:

Общие определения (DLL + exe) - вынести в отдельный модуль:
type
  // Ваша "структура"
  TSomething = record
    A: Integer;
    S: WideString;
  end;

  // Ваш "список структур"
  TSomethingArray = array of TSomething;

  // То, чем будут обмениваться DLL и exe: интерфейс
  IArray = interface
  ['{EE7F1553-D21F-4E0E-A9DA-C08B01011DBE}'] // нажмите Ctrl+Shift+G, чтобы сгенерировать идентификатор
  // protected
    function GetCount: Integer; safecall;
    function GetItem(const AIndex: Integer): TSomething; safecall;
  // public
    property Count: Integer read GetCount;
    property Items[const AIndex: Integer]: TSomething read GetItem; default;
  end;
DLL:
// DLL, служебный код:
type
  // Реализация IArray, которая будет возвращаться из DLL в exe
  TBaseArray = class(TInterfacedObject, IArray)
  protected
    FArray: TSomethingArray;
    function GetCount: Integer; safecall;
    function GetItem(const AIndex: Integer): TSomething; safecall;
  end;

  // Для статических и открытых массивов - копирование массива
  TArray = class(TBaseArray)
  public
    constructor Create(const AArray: array of TSomething);
  end;

  // Для динамических массивов можно сэкономить на копировании
  TArrayRef = class(TBaseArray)
  public
    constructor Create(const AArray: TSomethingArray);
  end;

  // Вместо трёх классов можно быть сделать и один (TArray), 
  // но с двумя конструкторами (CreateByArr, CreateByDynArr).
  // А можно и один конструктор, если вы всюду работаете только с одним типом массива.
  // Но не суть важно, как вы это сделаете - как вам удобнее. 
  // Тут главное, что у вас должен быть класс, реализующий интерфейс.

{ TBaseArray }

function TBaseArray.GetCount: Integer;
begin
  Result := Length(FArray);
end;

function TBaseArray.GetItem(const AIndex: Integer): TSomething;
begin
  Result := FArray[AIndex];
end;

{ TArray }

constructor TArray.Create(const AArray: array of TSomething);
var
  ArrIndex: Integer;
begin
  inherited Create;

  SetLength(FArray, Length(AArray));
  for ArrIndex := 0 to High(AArray) do
    FArray[ArrIndex] := AArray[ArrIndex];
end;

{ TArrayRef }

constructor TArrayRef.Create(const AArray: TSomethingArray);
begin
  inherited Create;

  FArray := AArray;
end;

// DLL, реальный прикладной код:

function DoSomething1: IArray; stdcall;
var
  A: array[0..2] of TSomething;
begin
  // Какая-то работа с массивом...
  A[0].A := 1;
  A[0].S := 'S1';
  A[1].A := 2;
  A[1].S := 'S2';
  A[2].A := 3;
  A[2].S := 'S3';

  // Возврат результата
  Result := TArray.Create(A); // <- здесь будет копирование массива
end;

function DoSomething2: IArray; stdcall;
var
  A: TSomethingArray;
begin
  // Какая-то работа с массивом...
  SetLength(A, 3);
  A[0].A := 1;
  A[0].S := 'S1';
  A[1].A := 2;
  A[1].S := 'S2';
  A[2].A := 3;
  A[2].S := 'S3';

  // Возврат результата
  Result := TArrayRef.Create(A); // нет копирования массива, только учёт ссылок
  // можно и:
  // Result := TArray.Create(A);
  // но тогда массив будет скопирован
end;

exports
  DoSomething1, DoSomething2;
exe:
function DoSomething1: IArray; stdcall; external 'Project2.dll';
function DoSomething2: IArray; stdcall; external 'Project2.dll';

procedure TForm1.Button1Click(Sender: TObject);
var
  A: IArray;
  X: Integer;
begin
  A := DoSomething1; // или DoSomething2

  for X := 0 to A.Count - 1 do
    OutputDebugString(PChar(IntToStr(A[X].A) + ' ' + A[X].S));
end;
Как видно (надеюсь) из примеров, использование интерфейсов - предпочтительный способ. Потому что всё делается само, автоматически. Вам не нужно следить за размерами (а вы можете ошибиться), вам не нужно работать с низкоуровневыми указателями (а вы можете перепутать указатели или испортить их), вам не нужно следить за вызовами функций (а вы можете что-то забыть сделать или вызвать не то) - короче, тут вообще ни о чём думать не надо. Разве не рай? :) Конечно, использование интерфейсов требует больше служебного кода со стороны передающего на классы-переходники (в данном примере - DLL, но это мог бы быть и exe, если делать передачу в обратную сторону через callback), но это с лихвой окупается удобством использования.

Примечание: иногда для обозначения динамической памяти вообще применяют термин heap (куча). Помимо собственно динамической памяти, этот термин имеет и другие значения. Подробнее.

См. также В чём разница между SHGetMalloc, SHAlloc, CoGetMalloc и CoTaskMemAlloc?

FAQ

Итак, давайте просуммируем сказанное в кратком FAQ:

1. Вопрос: Как мне правильно передавать динамические объекты между DLL и программой?
Ответ: Необходимо реализовать правило "кто выделил память - тот её и освобождает". Вы можете сделать это, используя общий менеджер памяти или следя за соблюдением правила вручную.

2. Вопрос: Как мне использовать общий менеджер памяти в программе?
Ответ: В файлы всех проектов DLL и программы первым модулем в список uses нужно добавить модуль, реализующий общий менеджер памяти. Например, ShareMem.

3. Вопрос: Что будет, если я впишу модуль общего менеджера памяти не первым?
Ответ: Будут происходить плохие вещи. Например, AV.

4. Вопрос: Что будет, если я впишу модуль общего менеджера памяти не во все проекты (например, только в программу, но не в DLL)?
Ответ: Это эквивалентно тому, что в вашей программе не будет использоваться общий менеджер памяти.

5. Вопрос: Могу ли я писать DLL на других языках, если я использую общий менеджер памяти типа ShareMem?
Ответ: Сильно зависит от языка и от реализации конкретного менеджера памяти. Часто ответ будет "нет".

6. Вопрос: А что будет, если я буду использовать DLL, написанную на другом языке без общего менеджера памяти?
Ответ: Общий менеджер памяти не используется => правило не выполняется => будут происходить плохие вещи (ситуация аналогична вопросу 4).

7. Вопрос: Если я руками слежу за соблюдением правила "кто выделил память - тот её и освобождает", даёт ли мне что-либо общий менеджер памяти?
Ответ: Нет.

8. Вопрос: А что будет, если я буду использовать DLL, написанную на другом языке без менеджера памяти? Но при этом я слежу за выполнением указанного правила?
Ответ: Правило выполняется (контролируется вручную) => всё будет работать корректно (ситуация аналогична вопросу 4).

9. Вопрос: Чем отличаются ShareMem, SimpleShareMem или другие аналогичные модуля?
Ответ: Это просто разные реализации одной идеи. Особенности работы и требования каждого модуля вы можете найти в справке. Наиболее очевидное различие: ShareMem требует наличия библиотеки менеджера памяти, а SimpleShareMem работает без дополнительных библиотек (на самом деле, SimpleShareMem вообще является простой "включалкой" функциональности общего менеджера памяти в менеджере памяти FastMM, который является менеджером памяти по-умолчанию в новых Delphi, и состоит он всего из десятка строк). Кстати, у SimpleShareMem есть особенность: RTL.SimpleShareMem.Невозможность разделения менеджера памяти под некоторыми версиями ОС.

10. Вопрос: Почему одни общие менеджеры памяти требуют таскать с собой дополнительную библиотеку, а другие - нет?
Ответ: Примерно по той же причине, почему гвозди забивают молотком, а шурупы завинчивают отвёрткой - это разные реализации одной идеи.

11. Вопрос: А если я соберу свою программу и библиотеку с пакетами?
Ответ: Поскольку при сборке с пакетами один и тот же модуль не может присутствовать в двух экземплярах (т.к. он включается в один пакет, а уж потом этот пакет используют все остальные), то и менеджер памяти будет в пакете всего один - т.е. тот, который сидит в rtlXXX.bpl. И именно его будут использовать библиотеки и exe-файл. В этом случае rtlXXX.bpl играет роль библиотеки общего менеджера памяти. Следовательно, правило в этом случае будет выполняться и всё будет работать корректно без дополнительных усилий.

12. Вопрос: А если я соберу свою программу и библиотеку с пакетами, даёт ли мне что-либо общий менеджер памяти?
Ответ: Нет, он не нужен. Почему - указано в вопросе 11.

13. Вопрос: Что будет, если главное приложение построено с пакетами, а библиотека - нет (например, написана на другом языке)?
Ответ: Те, кто собраны без пакетов - будут использовать свой отдельный менеджер памяти, не связанный с общим менеджером памяти, используемом модулями, построенными с пакетами. Следовательно, вы должны следить за выполненим правила вручную или будут происходить плохие вещи.

14. Вопрос: А что такое "ручное соблюдение правила"?
Ответ: Это значит, что вам нужно следить, чтобы память освобождал именно тот, кто её выделяет. Например, если кто-то вам вернул указатель на блок памяти, то вы не должны пытаться освободить его, передавая в какой-нибудь FreeMem, а передать тому, кто его выделил, с просьбой освободить память. Разумеется, для этого у чужой стороны должен быть предусмотрен какой-нибудь механизм, иначе это ошибка проектирования (и у вас будут постоянные утечки памяти). Если за проектирование подобного механизма отвечаете вы - то посмотрите в сторону интерфейсов: это самый простой для использования способ, хотя и требует написания более объёмного кода.

15. Вопрос: Функция из сторонней библиотеки (написанной на С) возвращает указатель Р на .... Собственно вопрос: как освободить занимаемую этими записями память? Функции типа dispose (Р) и FreeMem(Р) дают ошибку «Неверный указатель»
Ответ: В описании функции должно быть указано, как следует освобождать после неё память. Для этого часто из библиотеки экспортируется спец. функция очистки. Иногда в качестве таковой выступает системная, например, LocalFree. Иногда вы должны вызвать функцию из библиотеки, чтобы узнать размер памяти, а затем самостоятельно выделить память и вызвать эту же функцию второй раз, передавая уже подготовленный блок памяти.

Пытаться освободить память менеджером памяти Delphi - принципиально неверно, т.к. он не знает об этом блоке памяти, он ничего не сможет с ним сделать. И FreeMem, и Dispose и об-nil-ние интерфейсов и массивов, и очистка строк - все они вызывают одну и ту же стандартную системную функцию _FreeMem. Которая, по-умолчанию, вызывает функцию освобождения памяти стандартного менеджера Delphi MemoryManager.FreeMem. А библиотека и главная программа используют два разных, не связанных между собой менеджера памяти.

Короче говоря - надо читать документацию к библиотеке/функции, чтобы выяснить, как с ними следует работать (вот оно: отсутствие стандарта. К каждому приходится подходить индивидуально).

16. Вопрос: А можно ли тогда взять какой то чужой менеджер (например, из библиотеки) вместо стандартного делфийского?
Ответ: Можно. Для этого достаточно вызвать SetMemoryManager, предварительно вписав в структуру TMemoryManager указатели на управляющие функции.
Вопрос в другом: а где вы его возьмёте, этот чужой менеджер памяти? Библиотека писана на C. Там используется какой-то внутренний менеджер памяти, доступа к которому у вас нет никакого. Точно так же, как библиотека, писанная на Delphi, не выставляет наружу никаких функций (а особенно управление менеджером памяти), пока вы явно это не укажете в exports. Ну и как вы достучитесь до чужого менеджера памяти? Никак. Поэтому библиотека и должна предоставлять функцию освобождения памяти.

Но даже, если бы вдруг у вас на руках чудесным образом оказались бы указатели на чей-то чужой менеджер памяти, заменять им свой - крайне плохая идея. И вот почему. До того момента, как вы получили в свои руки чужой менеджер памяти и установили его, у вас уже выделена целая куча памяти. Любой блок может быть освобождён сразу после того, как вы установите новый менеджер. Но запрос-то на освобождение придёт не вашему менеджеру, а чужому! Который про этот блок памяти ничего не знает! Например:
var
  MM: TMemoryManager;
  S: String; // <- как-то используется для получения чужого MM
begin
  ... // <- как-то получаем чужой MM
  SetMemoryManager(MM);
end; // <- здесь наступает апокалипсис, потому что тут освобождается S.
// Она была выделена нашим менеджером памяти, а освобождается чужим
И наоборот. Поработали, выгружаем библиотеку. А при выгрузке менеджер памяти библиотеки финализируется и освобождает всю выделенную им память:
var
  S: String; 
begin
  ... // <- как-то используется S
  SetMemoryManager(OldMM); 
  FreeLibrary(DllHandle); // <- выгружаем DLL, 
                 // её менеджер памяти освобождает свою память
  ShowMessage(S); // <- AV, т.к. память под S была освобождена
  // или
  S := ''; // <- ещё одно ГГ, 
  // т.к. запрос на освобождение опять получил не тот MM
end;
Именно поэтому нужно чётко разграничивать: какие блоки памяти вы выделили сами, а какие - выделил вам чужой дядька. И свою память вы освобождаете сами, а чужую - отдаёте обратно дядьке на освобождение.

Да, если бы:
1). Библиотека была бы статически связана с вашим exe, а не грузилась бы динамически.
и
2). Менеджер памяти или в exe или в библиотеке заменялся бы на чужой автоматически загрузчиком ОС.
то использование чужого ММ было бы вполне безопасно.

Но это подразумевает, что:
1). Библиотеки нельзя будет загружать динамически!
2). Существует какой-то стандарт на MM ВСЕХ библиотек, писанных в ЛЮБЫХ языках (чего нет) (****).
3). Загрузчик ОС как-то определяет, кому отдать предпочтение: то-ли подпихнуть в exe менеджер памяти в библиотеке, то-ли подпихнуть в библиотеку менеджер памяти в exe. А если к тому же эти MM отличаются по эффективности в разы, то при неудачном выборе не миновать табуретки в мониторе.

Разделяемый менеджер памяти (типа ShareMem, SimpleShareMem или в виде пакета) решает эту задачу таким образом, что менеджер памяти устанавливается ПЕРВЫМ же действием в программе. При этом используется само-придуманный стандарт. Который могут использовать только те, кто про него в курсе. А это как правило, только Delphi или C++ Builder приложения. И то, разные реализации - это разные стандарты, а, следовательно, они несовместимы между собой.

Для дополнительного чтения:

Примечания:
(*) Очень подробно менеджер памяти в старых Delphi описывается здесь. Подобного описания FastMM (менеджера памяти в новых Delphi) мне найти не удалось. Нашёл только краткое описание. Во всех версиях Delphi код менеджера памяти располагается в файле GetMem.inc.

(**) Да, я в курсе про 3 Гб.

(***) Вообще говоря, использование отдельной DLL не является здесь необходимым условием. Просто этот вариант проще всего реализуется: нужно просто вызвать SetMemoryManager, указав в качестве функций те, которые располагаются в общей DLL. Т.е. в модуле используется простейший менеджер памяти в виде переходника, который все запросы переправляет общему менеджеру памяти. Если каждый модуль в программе это сделает, то получится, что в итоге все будут использовать одну DLL и один менеджер памяти в ней. Очень просто.

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

Более правильной реализацией было бы создание, скажем, куска именованной разделяемой памяти, которая взаимно-однозначно идентифицировалась бы с процессом. В неё можно записать указатели на функции менеджера памяти, а остальные модули могли бы открыть этот блок и использовать эти функции. Таким образом, все модуля ссылались бы (через этот блок памяти) на один менеджер памяти (не суть важно, в каком модуле он был установлен).

(****) Не уверен, как с этим обстоит дело в .NET.

19 комментариев :

  1. Спасибо!
    Не думал, что в самом Delphi предусмотрена возможность заменить менеджер памяти.

    ОтветитьУдалить
  2. Отдельное спасибо за FAQ!
    Это именно то, что чего не хватало "чайнику" во мне. =)

    ОтветитьУдалить
  3. очень познавательно. большое спасибо

    ОтветитьУдалить
  4. Вопросы начинающих по посту:

    http://forum.vingrad.ru/forum/topic-248818.html

    ОтветитьУдалить
  5. как же эту задачу решают программы вроде TestComplete или Delphi Peeper? ведь они могут показывать свойства Delphi компонентов

    ОтветитьУдалить
  6. эти приложения используют разные менеджеры памяти, но при этом получают данные от контролов (значения свойств, например) через RTTI. я так понимаю описанная им пришлось обходить проблему, описанную в данной статье

    ОтветитьУдалить
  7. Ну а вопрос-то в чём?
    Вы свободно можете читать любую доступную память в вашей программе из любого места. И не важно, кем она выделена. Менеджер памяти не имеет к этому никакого отношения.

    ОтветитьУдалить
  8. Немного грубова то даже для обьяснений начинающим...
    //Обычно размер страницы равен 4 Кб
    не очень усложнит понимание начинающим если написать что этот размер зависит от ОС...
    // 12 б * 100 = 1.2 Кб
    зачем же так? равно 1200 б
    а 1.2 Кб = 1.2 * 1024 = 1228,8 байт = 1228 байт 7 бит (т.к 0,8 байта = 8 бит * 0,8 = 6,4 т.к меньше бита выделить нельзя, следовательно 7 бит)
    а статья под ссылкой "утечка абстракции" честно говоря всего лишь бредовый фарс, просто попытка привлечь к какой то проблеме притянутыми примерами.

    ОтветитьУдалить
  9. >>> не очень усложнит понимание начинающим если написать что этот размер зависит от ОС...
    Ну-ка, назовите мне, при каких условиях для Delphi программы размер страницы будет отличен от 4 Кб?

    >>> зачем же так?
    Потому что для понимания сути не важно, что 1.2 Кб не равно в точности 1200 Кб. Просто (примерно) 2 Кб проще сравнить с (примерно) 400 Кб, чем (точно) 1200 + ? байт с (точно) 400000 байт. Для демонстрации идеи этого достаточно.

    ОтветитьУдалить
  10. В примере Sz := Length(Result) а также if Len < Length(Result) следует читать как Rslt? И скажите, что значит "наступает ГГ"?

    ОтветитьУдалить
  11. Оопс, опечаточка. Исправил, спасибо. Собственно, этот код я писал прямо в редакторе постов, не в Delphi.

    ГГ - зацензуренное нехорошее слово на ваш выбор. Мне казалось, там из контекста понятно, что происходят плохие вещи. Изменил формулировку.

    ОтветитьУдалить
  12. Вопрос по поводу FastShareMem, кое-где читал, что он не совсем хороший. Что посоветуете использовать ShareMem или FastShareMem(полностью ли он корректен?)

    ОтветитьУдалить
  13. Посоветую FastMM и не разделять менеджер памяти по возможности. Если без этого никак, то SimpleShareMem (ибо FastMM).

    ОтветитьУдалить
  14. Спасибо за статью.

    Почему вы ничего не сказали про Heap-функции операционной системы? Ведь именно они выполняют роль менеджера памяти, а не Virtual-функции.

    ОтветитьУдалить
  15. Ещё один вариант (установка для DLL того же менеджера памяти, что и в основной программе):

    [ TestLib.pas ]

    library TestLib;

    procedure SetMM(M: TMemoryManager);
    begin
    SetMemoryManager(M)
    end;

    function Test(S: String): String;
    begin
    Result := '['+S+']'
    end;

    exports
    SetMM, Test;

    end.

    [ Test.pas ]

    {$APPTYPE CONSOLE}
    procedure DLLSetMM(M: TMemoryManager); external 'TestLib' name 'SetMM';
    function Test(S: String): String; external 'TestLib' name 'Test';

    var M: TMemoryManager;
    begin
    GetMemoryManager(M);
    DLLSetMM(M);
    WriteLn(Test('Hello Word!'))
    end.

    Ну или вот так (по сути, аналогично, но проще для вызываемой программы):

    [ TestLib2.pas ]

    library TestLib2;

    procedure SetMM(GetMemoryManagerProc: Pointer);
    type
    TGetMemoryManagerProc = procedure(var TMemoryManager);
    var
    M: TMemoryManager;
    begin
    TGetMemoryManagerProc(GetMemoryManagerProc)(M);
    SetMemoryManager(M)
    end;

    function Test(S: String): String;
    begin
    Result := '['+S+']'
    end;

    exports
    SetMM, Test;

    end.

    [ Test2.pas ]

    {$APPTYPE CONSOLE}
    procedure DLLSetMM(GetMemoryManagerProc: Pointer); external 'TestLib2' name 'SetMM';
    function Test(S: String): String; external 'TestLib2' name 'Test';

    var M: TMemoryManager;
    begin
    DLLSetMM(@GetMemoryManager);
    WriteLn(Test('Hello Word!'))
    end.

    p.s. А можно тут код как-то выделять красиво (как на форумах теги [code][/code])? :))

    ОтветитьУдалить
    Ответы
    1. Это пример для Delphi7. Для новых немного по-другому будет, но суть та же...

      Удалить
    2. Тэга типа code/pre - нет, увы.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.