ReallocMem — Процедура Delphi

Содержание

ReallocMem — Процедура Delphi

Основные процедуры для работы с динамическими переменными приведены ниже.

Dispose Высвобождает память из-под динамической переменной.
Finalize Деинициализирует динамическую переменную.
FreeMem Высвобождает память из-под динамической переменной.
GetMem Создает динамическую переменную, выделяя под нее указанный объем памяти.
Initialize Инициализирует динамическую переменную.
New Создает динамическую переменную.
ReallocMem Перераспределяет память для динамической переменной.
Процедура Dispose( var P: Pointer);

Описание:
Процедура высвобождает область памяти, которую использует динамическая переменная P. Значение указателя P в данном случае становится неопределенным. Если функции передан недопустимый указатель, то возникает исключение EInvalidPointer. Обработка ошибок с помощью механизма обработки исключительных ситуаций включается директивой компилятора <$I+>.

Пример: Процедура Finalize( var V [; Count: Integer] );

Описание:
Процедура деинициализирует динамическую переменную, указанную в параметре V. Данная процедура должна использоваться только в тех случаях, когда для высвобождения памяти из-под динамической переменной не используется процедура Dispose. Для объектов глобальных, локальных и динамических переменных при высвобождении памяти с помощью стандартной процедуры Dispose, компилятор генерирует код, завершающий работу с длинными строками, переменными типа Variant и интерфейсами после разрушения переменной. Если память, содержащая не пустые или не инициализированные длинные строки, Variant-переменные или интерфейсы, высвобождается не при помощи процедуры Dispose (например, процедурой FreeMem), то перед высвобождением памяти требуется вызвать процедуру Finalize для того, чтобы закрыть указанную переменную. Процедура Finalize присваивает всем длинным строкам пустое значение, а переменные типа Variant и интерфейсы деинициализирует (устанавливает тип Unassigned). Дополнительный параметр Count может быть определен в тех случаях, когда необходимо высвободить память, из-под нескольких переменных, содержащихся в непрерывном блоке памяти (например, динамически распределенный массив строк) для того, чтобы закрыть все переменные одной операцией. Если переменная, определенная в параметре V не содержит длинных строк, Variant-значений и интерфейсов, то компилятор просто игнорирует вызов процедуры.

Пример:
См. пример к функции FreeMem.

Процедура FreeMem( var P: Pointer [; Size: Integer] );

Описание:
Процедура уничтожает переменную, с которой связан указатель P и высвобождает память, занимаемую данной переменной. В необязательном параметре Size указывается объем памяти в байтах, выделенный ранее динамически под переменную. Если после действия процедуры FreeMem, вызвать указатель P, то возникнет ошибка, т.к. указатель имеет неопределенное значение.

Пример: Процедура GetMem( var P: Pointer; Size: Integer );

Описание:
Процедура создает динамическую переменную: выделяет блок памяти размером Size байт под переменную, указанную в параметре P, и возвращает указатель на начало данного блока памяти. Параметр P может представлять собой любой тип указателя. Указатель на новую созданную переменную записывается как P^. Если для создания динамической переменной недостаточно памяти, то возникает исключение EOutOfMemory.

Пример:
См. пример к функции FreeMem.

Процедура Initialize( var V [ ; Count: Integer] );

Описание:
Процедура инициализирует динамическую переменную. Если динамическая переменная была создана не с помощью процедуры New, а другим способом (например, с помощью процедуры GetMem или процедуры ReallocMem), то после создания переменной, ее необходимо инициализировать процедурой Initialize. При вызове данная процедура обнуляет память, занятую длинными строками Variant-значениями и интерфейсами. Длинным строкам присваивается пустое значение, а для Variant-значений и интерфейсов устанавливается неопределенный тип (Unassigned). Необязательный параметр Count может быть определен, когда память под несколько переменных выделена в непрерывном адресном пространстве. Это позволяет инициализировать все переменные одним вызовом процедуры. Если переменная, определенная в параметре V не содержит длинных строк, Variant-значений и интерфейсов, то компилятор игнорирует данный вызов процедуры и не генерирует ни какого кода.

Процедура New( var P: Pointer );

Описание:
Процедура создает новую динамическую переменную, и ассоциирует с ней указатель P. Параметр P может представлять собой любой тип указателей. Размер памяти, выделяемый под переменную, зависит от типа указателя. Новая созданная переменная может быть вызвана как P^. Если для создания динамической переменной недостаточно памяти, то возникает исключение EOutOfMemory. По завершению использования динамической переменной память, выделенную ранее процедурой New, необходимо высвободить вызовом процедуры Dispose.

Пример:
См. пример к функции Dispose.

Процедура ReallocMem( var P: Pointer; Size: Integer );

Описание:
Процедура перераспределяет память размером Size байт под динамическую переменную P. При вызове данной процедуры указатель P должен иметь значение nil или должен указывать на динамическую переменную, память под которую была предварительно выделена с помощью процедур GetMem или ReallocMem.Если P = nil, Size = 0, то процедура не производит никаких действий.Если P = nil, а Size <> 0, то процедура распределяет новый блок памяти размером Size и устанавливает указатель P на начало блока. Такой вызов процедуры аналогичен обращению к процедуре GetMem. Если P <> nil, а Size = 0, то процедура высвобождает блок памяти, на который указывает P и устанавливает P = nil. Вызов процедуры с указанными параметрами аналогичен обращению к процедуре FreeMem, но в отличие от FreeMem процедура ReallocMem очищает указатель.

ReallocMem — Процедура Delphi

4 ГБ, которые я разумеется в память целиком не подгружаю, только по 1 файлу) приложение начало вываливаться с Out of memory. Наверное правда переполняется, подумал я, и начал ковырять. Просидел весь день, повыбрасывал из кода все (сканирование файлов, обработку данных) и вот что оказалось в сухом остатке.

2 секунды, после чего занимает аж 125 метров в памяти! Если же скомпилить его в Delphi 2007, выполняется мгновенно и занимает в памяти 2 метра! Скорее всего виновата работа с памятью при использовании String’ов (то ли MemoryManager, то ли реализация copy-on-write хромает)

ЗЫ. Есть подозрение, что данная проблема отчасти коррелирует с вот этой (http://forum.ixbt.com/topic.cgi? >

1. Spirit-1 , 04.09.2010 18:26
Проблема касается вот этой строки:

При этом идёт выделение нового блока памяти. Используйте хотя бы увеличение размера массива не на один элемент, а блоками (пусть по 100 элементов). Увидите большую разницу.

P.S. Хотя что-то и у меня закрались подозрения. Не люблю я этот тип String, он динамический, а динамический массив стрингов — вообще непонятный зверь. Попробуйте свой пример с типом ShortString.

2. evatutin , 04.09.2010 19:51
Spirit-1
Проблема касается вот этой строки

Я знаю, что эта строка ведет к reallock’у при каждом новом добавлении, но в данном примере дело не в этом. Дело именно в использовании String, с ShortString все летает и там, и там (Delphi 7/2007)

3. naa , 04.09.2010 20:22
Ну а если array of заменить на TStringList, то тоже все летает и памяти ест

1 Мб. Так что не все так однозначно.

4. Spirit-1 , 04.09.2010 20:28
StringList не создаёт массива строк, он умнее ТС
Есть предположение, что в Delphi 7 при динамическом увеличении массива перераспределяется и место под строки, т.е. они все пересоздаются, а в 2007-ых просто копируются ссылки на предыдущие копии строк. Если интересно, то могу это посмотреть под отладчиком.

В целом же, уважаемый ТС, проблема высосана из пальца. Правильно сказали — используйте StringList. Ну или на крайний случай, если возможно, ShortString.

5. Gipnoss , 04.09.2010 21:13
Spirit-1
они все пересоздаются
и не освобождаются.
Выглядит именно как проблема, и ее даже в следующих версиях пофиксили как проблему.
6. Spirit-1 , 04.09.2010 21:27
Gipnoss
Выглядит именно как проблема
Как проблема здесь выглядит динамический массив динамических строк. Вот это действительно проблема, причём проблема ТС, а никак не дельфей. Только без обид. Нельзя использовать вещи, не понимая их сложности. А если понимаете, то её (сложность) надо уменьшать ((C) Макконнелл).

Добавление от 04.09.2010 21:32:

P.S. И, кстати, засовывать String в Record тоже не рекомендовал бы. Поскольку вдруг потом захочется их (записи) присваивать друг другу, получать размер и сохранять в поток, да мало ли чего захочется. И всё это может, а скорее всего и пойдёт вкривь и вкось.

7. fedya333 , 04.09.2010 23:12
Да уж. Все что вы пишите это бред. Полное незнание матчасти, не понимание работы ни массивов, ни работы со строками, ни глобальных переменных.
Программа работает абсолютно правильно, а вот написана она криво.
А в том случае, когда она не жрет память — видимо просто работает оптимизация и переменная массив просто выкидывается.

После таких вот «тредов», таких вот «программистов» многие тыкают пальцем на Д и говорят какая она кривая.
Это не она кривая, это вы кривые, неучи.

«бойтесь работы со строками!»
«Не люблю я этот тип String, он динамический, а динамический массив стрингов — вообще непонятный зверь. «Попробуйте свой пример с типом ShortString.»
«Как проблема здесь выглядит динамический массив динамических строк. Вот это действительно проблема»
«И, кстати, засовывать String в Record тоже не рекомендовал бы»

8. Spirit-1 , 05.09.2010 01:10
Детская забава — зарегистрироваться под новым ником и написать какую-нибудь гадость. Ай-ай-ай.
Кстати, прошу прощения у автора темы, если чем-то задел его.

Добавление от 05.09.2010 01:23:

fedya333
Уважайте других. Только зарегистрировались, а уже пиарите Delphi везде. И здесь, кроме наездов непонятно на кого, ничего дельного не написали.

9. naa , 05.09.2010 04:33
Gipnoss
Выглядит именно как проблема, и ее даже в следующих версиях пофиксили как проблему.
Надо еще учитывать особенности менеджера памяти (а в Delphi он свой), который мог зарезервировать и не отдать системе память. Например, если в конце цикла поставить Arr := nil; то память чудесным образом возвращается системе.
10. AzikAtom , 05.09.2010 12:09
Исходная версия выполнилась за 18 сек, заняла 128 МБ памяти в конце выполнения.

Задал массив arr размером 10000 элементов. Выполнилось за десятые доли секунды и заняло память 2 МБ.

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

11. evatutin , 05.09.2010 17:48
TStringList на мой взгляд здесь не катит, т.к. есть массив записей, из полей которых я оставил только FileName, чтобы не загромождать пост. Конечно можно сделать вместо него несколько массивов, а вместо одного из них — StringList, но это кому как нравится. Мне — нет

Spirit-1
Конструктивный спор, какие обиды

fedya333
А в том случае, когда она не жрет память — видимо просто работает оптимизация и переменная массив просто выкидывается.

Вы готовы предложить «правильный» вариант?
Я делюсь впечатлениями от проблемы, на которую напоролся на ровном месте, анализируя чрезмерные затраты памяти. Сперва думал, что у меня leak, общупал код MemCheck’ом, а когда понял, что проблема ведет в недра Delphi, стал ковырять и вот результат. Что здесь «кривого» и «неправильного»?

All
Массив записей не планировалось сохранять в файл (это временная информация), поэтому динамический массив динамических строк на мой взгляд лучше, чем потенциальная нехватка длины строки (никогда не замечали, как WinRar иногда отказывается архивировать папки и файлы с длинными именами — скорее всего MAX_PATH виновата и статические буферы. )

12. naa , 05.09.2010 18:11
evatutin
поэтому динамический массив динамических строк на мой взгляд лучше
Про TList что-нибудь слышал?

Что здесь «кривого» и «неправильного»?
Тот пример в начале — классический быдло-код.

13. fedya333 , 05.09.2010 18:47
evatutin
Ага. проблема делфи

Скажите мне теперь, в каком месте приведенный код, по вашему должен освобождать память и почему он её не должен жрать по нарастающей?
я ощибаюсь

14. qq1 , 05.09.2010 21:46
fedya333
почему он её не должен жрать по нарастающей?

В Delphi 2007 не жрёт.

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

15. evatutin , 05.09.2010 22:23
naa
Про TList что-нибудь слышал?

Разумеется. Пользую в критически важных участках, а когда лень — обычный динамический массив. Я в свое время подробно интересовался вопросом reallock’ов: если массив небольшой, то и разница в скорости не сильно заметна. Хотя, согласен, TList в данной ситуации использовать идеологически правильнее.

fedya333
На мои вопросы вы не ответили, имхо ценного ничего в дискуссию не привнесли. Стоит продолжать?

qq1
Ошибок и утечек, конечно, здесь нет. Есть неэффективность связанная с неэффективностью менеджера кучи

Разумеется, но на начало анализа я этого не знал . Мне попался достаточно простой пример, подтверждающий это

Добавление от 05.09.2010 22:28:

Посмотрел посты fedya333 в других ветках — не менее интеллектуально, чем здесь! Может все-таки стоит продолжить дискуссию — узнаем много нового.

16. AlexNek , 05.09.2010 23:20
evatutin
Поясните.
Да за подобные вещи сразу на Колыму надо отправлять Неужели в дельфях нет нормальных динамических массивов, у которых есть метод Add?

21. naa , 06.09.2010 01:43
evatutin
Поясните.
Ну я то думал ты это сам понимаешь.

Вот здесь же сам написал:
Я знаю, что эта строка ведет к reallock’у при каждом новом добавлении

а когда лень — обычный динамический массив
Если уж неохота заморачиваться с указателями и ручным освобождением памяти, то можно посмотреть на TObjectList

Объявляем вместо записи класс:

Всяко лучше, чем неправильное использование динамического массива

22. moderator-Kid , 06.09.2010 08:39
fedya333
Я тут почистил маленько Ваш поток откровений и предупреждаю, будете продолжать в таком духе — доступ на форум Вам будет закрыт.
23. sla3 , 06.09.2010 09:48
моё имхо — в Delphi7 процедура SetLength неправильно работает с динамическими массивами записей.
Кошерный ли код примера — это уже другой вопрос.

24. evatutin , 06.09.2010 09:49
AlexNek
Неужели в дельфях нет нормальных динамических массивов, у которых есть метод Add?

Может есть, а я не знаю? (На всякий случай: TList — это список, а не динамический массив, реализованный правда на базе динамического массива)

naa
Объявляем вместо записи класс

И при каждом добавлении записи у нас вместо reallock’а работает allock под новую запись при создании объекта, инкапсулирующего мою запись . И объекты в динамической памяти будут разбросаны скорее всего, что приведет к неэффективной работе кэша (а в массиве записей они подряд лежат). Еще неизвестно, что будет быстрее. Если бы в Delphi классы были статическими, тогда да, но.
Если уж на то пошло, то лучше действительно сперва узнать число объектов (про это выше писал qq1), сделать один SetLength(), а потом методично заполнять.

25. Tapa , 06.09.2010 09:57
evatutin
В качестве оффтопика — realloc (без k на конце).
26. sla3 , 06.09.2010 09:58
evatutin
кстати пример можно ещё более упростить —
Arr: array of string;

применение
SetLength(Arr, Length(Arr)+1);
даёт такой же эффект

27. Artemiy , 06.09.2010 10:19
Вообщем — это фрагментация кучи. Из-за множественных релоков. Особенность менеджера памяти и особенность работы конкретного кода.

В Delphi2007 по-другому, потому, что там используется продвинутый менеджер памяти FastMM.

Вы можете использовать его и в Д7, тогда поведение программы будет как в Д2007. Данный менеджер дает минимальный эффект фрагментации и вообще очень продвинутый.

Добавление от 06.09.2010 10:21:

Так же правильным решение в данном случае будет либо заранее узнать размер необходимого массива, либо добавлять его размер большим шагом «наперед» и в процессе добавления контролировать емкость (что и сделано во всех стандартных списках, включая TList)

28. sla3 , 06.09.2010 10:32
Artemiy
Вообщем — это фрагментация кучи. Из-за множественных релоков. Особенность менеджера памяти и особенность работы конкретного кода.
Действительно, согласен. Выделилось место под string, потом идёт реаллокация массива на новое место (старое висит дыркой) и т.д.
Так тогда Delphi7 вообще нельзя применять, т.к. такая ситуация сплошь и рядом!
PS Таки программа не кошерная ибо порождает толкотню в памяти

Добавление от 06.09.2010 11:02:

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

Добавление от 06.09.2010 12:14:

безобидный пример, всего один дин.массив целых чисел —

Добавление от 06.09.2010 12:31:

что-то меня торкнуло — решил проверить что в MSVC 6.0 —

— никаких проблем

29. Artemiy , 06.09.2010 12:57
uses FastMM, мозг;

И никаких проблем.

30. AzikAtom , 06.09.2010 14:38
evatutin
И при каждом добавлении записи у нас вместо reallock’а работает allock под новую запись при создании объекта
В Delphi 4 как раз reallocmem

И объекты в динамической памяти будут разбросаны скорее всего, что приведет к неэффективной работе кэша
В силу того, что объекты прямого отношения к вашему tlist не имеют никакого отношения, то и разброс от tlist никак не зависит.
Сугубое imho, т.н. фрагментация памяти не влияет на скорость сколь-нибудь заметно.

31. AlexNek , 06.09.2010 21:34
evatutin
Может есть, а я не знаю?
Это мне просто интересно стало. Во многих «языковых библиотеках» есть готовые классы, а в Дельфях получается нет. Почему?
32. Gipnoss , 06.09.2010 21:50
AlexNek
Во многих «языковых библиотеках» есть готовые классы, а в Дельфях получается нет. Почему?
Потому что в Делфях динамические массивы — это не класс. Хотите класс — сделайте или поищите готовый.
P.S. Искать готовый, возможно, придется достаточно долго, т.к. шаблонов, без которых невозможно сделать add для неизвестного типа, тоже нет.
33. Artemiy , 06.09.2010 22:35
Ага. ничего нет.
А ничего, что давно есть TList?
А ничего, что уже пару лет есть шаблоны? (начиная с версии 2009)

ЗЫ всю жизнь обходился TList и dynamic array.

34. AlexNek , 06.09.2010 23:30
Artemiy
Ну если есть, зачем тогда самому что-то придумывать?

А есть возможность сделать что-то типа этого? Иначе говоря типизированный массив.
function Add(Item: MyClass): Integer

property Items[Index: Integer]: MyClass;

Уж и не помню когда дельфи в последний раз видел, просто интересно

35. naa , 06.09.2010 23:48
AlexNek
А есть возможность сделать что-то типа этого?

Наследуемся от TObjectList и перекрываем соответствующие методы. В более поздних версиях, как уже упоминали, появились Generic’и.

Добавление от 06.09.2010 23:49:

AlexNek
Ну если есть, зачем тогда самому что-то придумывать?
А это надо у автора темы спрашивать

Добавление от 06.09.2010 23:59:

sla3
Боже мой, с чем я работаю
А ты часто пишешь такой же код, как в первом посте? Тогда надо сокрушаться по другому поводу Проблема то искусственная. К тому же есть вполне адекватное решение — FastMM.

36. AlexNek , 07.09.2010 00:40
naa
В более поздних версиях, как уже упоминали, появились Generic’и
Тогда я совсем не понимаю приведенный код
37. naa , 07.09.2010 00:44
AlexNek
Тогда я совсем не понимаю приведенный код
Какой и в чем именно?
38. AlexNek , 07.09.2010 01:40
naa
Ну тот что в начале темы приведен.
39. Spirit-1 , 07.09.2010 01:43
Что-то никак не пойму, почему всё свалилось на TList, классы и т.п.
Во-первых, для целей автора есть нормальный класс: TStringList. Это как раз так нужный ему список строк.
Во-вторых, (возвращаясь в нашему примеру) дело действительно оказалось в реаллоках. Посмотрел под дебаггером и резюмирую: никаких багов в системной библиотеке Delphi нет, а есть сильная фрагментация памяти. Попытаюсь описать процесс для тех, кто вдруг не понимает сути:

Комментарий 1. Массив String’ов — это массив указателей. Каждая строка — это динамически выделенный блок памяти, содержащий саму строку, два нулевых байта в конце, 4х байтовый счетчик перед началом строки + 4х байтовый счетчик ссылок на строку.
Комментарий 2. При увеличении размерности массива выделяется новый блок памяти, содержимое старого блока копируется в новый, старый блок памяти освобождается. Вот только не помню уже, добавляется вновь свободный блок памяти в начало списка всех свободных блоков памяти или в конец.

Теперь основная проблема — идёт ПОПЕРЕМЕННОЕ выделение памяти — а) увеличение размерности массива; б) создание новой строки. Базовый менеджер памяти Delphi7 при это создаёт кучу дыр, неиспользованных блоков и т.п.

Сейчас проверю с FastMM.

40. naa , 07.09.2010 02:03
AlexNek
Ну тот что в начале темы приведен.
Ну а что там не понятного? Классический пример «как не надо использовать динамические массивы»

Spirit-1
Это баг компилятора, и замена менеджера памяти здесь ничего не изменит
Еще раз: если присвоить nil переменной массива, то память возвращается системе. Нет утечки.

Проверить под дебагером? Это я могу =)
Вперед и с песней!

41. Spirit-1 , 07.09.2010 02:21
Поправил предыдущий пост.

naa
Еще раз: если присвоить nil переменной массива, то память возвращается системе. Нет утечки.
Утечки нет (вернее мы тут говорим не об утечке, а о сильной фрагментации памяти), поскольку в процесс не вмешивается выделение памяти под строку.

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

Автор: Андрей Мистик
Источник: RSDN Magazine #2

Опубликовано: 21.02.2003
Исправлено: 13.03.2005
Версия текста: 1.0

Введение

Многие стандартные типы данных, такие, как классы, интерфейсы, строки, динамические массивы, неявно работают с динамической памятью. Зачастую пользователь даже и не подозревает, сколько обращений к динамической памяти происходит в той или иной строчке кода. Размеры объектов в динамической памяти могут колебаться от нескольких байт (строки) до многих мегабайт (пользовательские вызовы функций GetMem и AllocMem, а также создание динамических массивов). Достаточно трудно представить себе, какое количество строк и объектов может находиться в памяти во время работы программы. Естественно, что в такой ситуации требуется наличие быстрого и экономичного менеджера памяти, какой и предоставляется Delphi. Приведу цитату Евгения Рошаля, который в описании новых возможностей своей программы Far (версия 1.70 beta 2, сборка 321 от 16.12.2000) писал: «Для компиляции FAR Manager использовался Borland C/C++ 5.02. MSVC 6 SP4 не оправдал ожиданий (FAR 1.70 beta 1) и добавил тормозов (работа с выделением памяти для мелких объектов)». Известно, что менеджеры памяти в Borland C/C++, Borland C++ Builder и Delphi имеют общие алгоритмы работы. В данной статье я постараюсь в общих чертах описать принципы работы менеджера памяти Delphi.

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

Вся реализация менеджера памяти (за исключением некоторых объявлений в интерфейсной части модуля System.pas) расположена в файле GetMem.inc, хранящемся в каталоге $(DELPHI)/Source/Rtl/Sys. Менеджер памяти Delphi полностью реализован на Object Pascal, без использования ассемблерных вставок. Заметим еще, что в Kylix нет своего менеджера памяти, поэтому вызовы GetMem, FreeMem и ReallocMem сводятся к соответствующим вызовом malloc, realloc и free из стандартной C-библиотеки.

Для понимания работы менеджера памяти Delphi вам понадобятся знание принципов работы виртуальной памяти в Windows, а также понимание принципов работы четырех функций: VirtualAlloc, VirtualFree, LocalAlloc и LocalFree. Эта информация выходит за рамки данной статьи, и может быть получена либо из книги Джеффри Рихтера «Windows для профессионалов, 4-е издание», либо из MSDN.

Прежде всего, условимся о смысле используемых терминов. Основная путаница возникает из-за того, что менеджер памяти Delphi сам использует услуги менеджера виртуальной памяти Windows. Блок памяти, свободный с точки зрения программы, может считаться выделенным с точки зрения Windows. Итак, фрагмент памяти будем называть свободным, если он целиком состоит из страниц свободной памяти в смысле Windows. Фрагмент памяти будем называть зарезервированным, если он целиком состоит из страниц памяти, зарезервированных приложением. Фрагмент памяти будем называть выделенным, если он состоит целиком из страниц памяти, под которые выделена физическая память. Соответственно, фрагмент памяти будем называть не выделенным, если он состоит из страниц, зарезервированных приложением, но под которые не выделено физической памяти. Фрагмент памяти будем называть используемым, если он был передан Delphi-программе. И, наконец, фрагмент памяти будем называть неиспользуемым, если под ним расположена физическая память, но в настоящее время он не используется приложением Delphi. Вся иерархия памяти представлена на рис. 1.

Структура менеджера памяти Delphi

Менеджер памяти Delphi состоит из четырех администраторов, а также отладочных и диагностических функций. Схема зависимости администраторов представлена на рисунок 2. Расскажем коротко о каждом из них.

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

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

Главной функцией администратора выделенной памяти является обработка запросов по выделению и освобождению памяти. Он хранит также информацию обо всей зарезервированной, но еще не выделенной памяти.

Администратор используемой памяти является самым низкоуровневым администратором. Он непосредственно общается с самим приложением Delphi, удовлетворяя его запросы на выделение и освобождение памяти. Он хранит в себе информацию обо всей выделенной памяти.

Одной из особенностей работы всех администраторов (кроме администратора описателей блоков) является то обстоятельство, что они работают по принципу «можно больше, нельзя меньше». При запросе администратор может выделить больше запрашиваемого количества памяти, и уже администратор более низкого уровня должен решать, как ему использовать остаток. Так, скажем, при попытке зарезервировать область памяти размером 100 байт (обращение от администратора выделенной памяти к администратору зарезервированной памяти), все равно будет зарезервирован регион в 1 Мб, и администратор выделенной памяти сам должен будет решить, как ему поступать с остатком. Соответственно при попытке освобождения памяти администратор может освободить меньший фрагмент.

Сделаем также ряд замечаний по поводу схемы обработки ошибок менеджером памяти Delphi. Если функция возвращает выделенный блок памяти, то в случае ошибки возвращается блок со значением addr, равным nil. В случае ошибки при освобождении памяти устанавливается глобальная переменная heapErrorCode. Кроме того, ряд функций, возвращающих значение типа Boolean, сигнализируют о случившейся ошибке, возвращая значение False.

Опишем еще пару переменных – initialized, которая проверяет, инициализирован ли менеджер памяти, и heapLock – критическую секцию для обеспечения синхронизации при многопоточности. Отметим, что вход в критическую секцию происходит только в том случае, когда глобальная переменная IsMultiThread установлена в True. Это одна из причин, по которым потоки должны создаваться вызовом функции BeginThread, а не напрямую при помощи функции CreateThread.

Администратор описателей блоков

Почти вся информация о зарезервированной, выделенной и невыделенной памяти хранится в виде кольцевых двунаправленных списков. Главным их преимуществом является то, что для удаления элемента из такого списка достаточно знать лишь указатель на этот элемент (см., например, функцию DeleteBlock). Администратор описателей блоков предоставляет остальным администраторам функции для работы с такими списками, а также для динамического выделения в памяти элементов этих списков.

В листинге 1 приведены все объявления, относящиеся к данному администратору.

Обратим внимание на то, что менеджер памяти Delphi различает такие понятия, как блок (TBlock) и описатель блока (TBlockDesc). Блок задает фрагмент памяти и содержит в себе указатель на начало фрагмента и его длину. Блоки используются в основном для передачи параметров и при объявлении локальных переменных. Время жизни блока обычно ограничивается той процедурой или функцией, где он объявлен. Описатель блока – это элемент двунаправленного списка, который описывает некоторый фрагмент памяти.

Элементы кольцевых двунаправленных списков, кроме указателей на предыдущий и последующий элементы, хранят также информацию о начальном адресе фрагмента памяти и его длине. Обычно в таких списках можно выделить корневой элемент. Это элемент списка, который содержит нулевые значения в адресе и размере блока, и служит, с одной стороны, началом списка, и, с другой стороны, индикатором конца списка. Если у кольцевого двунаправленного списка есть корневой элемент, он не может быть пустым, т. е. он должен содержать по крайней мере один этот корневой элемент. Все элементы кольцевого двунаправленного списка можно обойти в цикле, приведенном в листинге 2.

Сразу отметим, что кольцевые двунаправленные списки, которые используются менеджером памяти Delphi, бывают двух типов. Основное различие типов – в способе выполнения операций вставки блока в список и удаления блока из списка. При добавлении блока в список первого типа, остальные блоки в списке остаются без изменений. Как следствие, удалить из такого списка мы можем лишь тот блок, который был вставлен в него ранее. При добавлении блока в список второго типа проверяется, не граничит ли добавляемый блок с остальными блоками в списке. Если это так, то происходит «слияние» блоков, т. е. все граничащие между собой блоки заменяются одним. Соответственно, удалить из такого списка можно любой блок памяти, лишь бы он содержался внутри одного из блоков, присутствующих в списке. В этом случае может произойти «расщепление» блока на два. Использование списков второго типа позволяет избежать фрагментации памяти.

Рассмотрим теперь динамическое выделение памяти под описатели. Все данные, относящиеся к администратору описателей блоков, хранятся в виде двух однонаправленных списков. В первом из них (blockDescBlockList) хранятся сами описатели группами по сто. Во втором списке (blockDescFreeList) хранятся свободные в настоящий момент описатели, а в качестве связи между ними используется поле next описателя. Каждый из описателей находится в одной из структур типа TBlockDescBlock списка blockDescBlockList, а если описатель свободен, то также и в списке blockDescFreeList. Типичное состояние администратора описателей блоков приведено на рисунке 3.

Кратко опишем функции администратора описателей блоков.

Данная функция создает новый описатель блока.

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

Данная функция динамически создает описатель для блока b и добавляет его (описатель) непосредственно перед элементом prev. В качестве элемента prev в текущей реализации менеджера памяти всегда используется указатель на корневой элемент списка первого типа. Таким образом, функция добавляет описатель блока в начало списка. В случае успеха функция возвращает True.

Данная процедура удаляет описатель блока из списка первого типа и освобождает этот описатель.

Данная функция добавляет указанный блок памяти в двунаправленный кольцевой список второго типа (см. выше). Возвращаемым значением является блок памяти, образовавшийся в результате слияния добавляемого блока с уже существующими. В случае ошибки значение addr в возвращаемом блоке установлено в nil.

Данная функция удаляет блок памяти из кольцевого двунаправленного списка второго типа. В случае успеха функция возвращает True.

Администратор зарезервированной памяти

Администратор зарезервированной памяти содержит информацию обо всех обращениях менеджера памяти к функции VirtualAlloc с целью резервирования фрагмента памяти. Эта информация нужна для того, чтобы в дальнейшем производить передачу физической памяти региону зарезервированного адресного пространства, а также чтобы производить освобождение памяти. Напомним, что при освобождении фрагмента памяти с помощью функции VirtualFree необходимо указать в точности тот адрес, который был получен функцией VirtualAlloc при резервировании этого фрагмента. Кроме того, за один вызов VirtualAlloc мы можем передавать физическую память только внутри диапазона адресов, зарезервированных одним вызовом VirtualAlloc. Поэтому при хранении информации о памяти, зарезервированной менеджером памяти Delphi, используются списки первого типа. Ведь иначе при слиянии двух блоков мы потеряем адреса, которые возвращались функцией VirtalAlloc, а значит, не сумеем впоследствии освободить память. Кроме того невозможно будет корректно передать физическую память фрагментам, лежащим в смежных областях, выделенных при помощи двух разных вызовов VirtualAlloc.

Этой особенностью поведения менеджера виртуальной памяти Windows объясняется и присутствие в администраторе зарезервированной памяти функций Commit и Decommit, которые производят соответственно передачу и возврат физической памяти со страниц зарезервированного фрагмента, но не имеют ограничений, присущих функциям VirtualAlloc и VirtualFree. Но для корректного выполнения таких операций необходимо обладать информацией обо всех вызовах функции VirtualAlloc с целью резервирования адресного пространства. Наличие этих функций в администраторе зарезервированной памяти также позволило выделить все обращения к менеджеру виртуальной памяти Windows в один администратор.

В листинге 3 приведены все объявления, относящиеся к данному администратору.

Таким образом, администратор зарезервированной памяти содержит единственный кольцевой двунаправленный список spaceRoot, в котором хранится описание всех обращений к функции VirtualAlloc с целью резервирования памяти. Первоначально этот список не содержит элементов. При работе администратора используются константы:

  • cSpaceAlign, показывающая, по какой границе будут выравниваться резервируемые фрагменты памяти;
  • cSpaceMin, показывающая минимальный размер блока памяти, который будет зарезервирован;
  • cPageAlign, задающая размер страницы.

Коротко опишем назначение и особенности каждой из функций, входящих в состав администратора.

Резервирует регион памяти размером, как минимум, равным minSize байт. Возвращает блок памяти, который был фактически зарезервирован. В случае ошибки возвращает в поле addr блока памяти значение nil. Зарезервированная память имеет атрибут доступа PAGE_NOACCESS.

Резервирует регион памяти по указанному адресу addr размером, как минимум, равным minSize байт. Возвращает блок памяти, который был фактически зарезервирован. В случае ошибки возвращает в поле addr блока памяти значение nil. Зарезервированная память имеет атрибут доступа PAGE_READWRITE.

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

Пытается освободить по указанному адресу addr не более чем maxSize байт. Возвращает блок памяти, который был фактически освобожден. Если память не освобождалась, функция возвратит в поле addr блока памяти значение nil. В случае ошибки устанавливает переменную heapErrorCode в значение cReleaseErr.

Функция выделяет фрагмент памяти по указанному адресу addr размером не менее minSize байт. В случае ошибки возвращает в поле addr блока памяти значение nil. Выделенная страница памяти будут иметь атрибуты доступа PAGE_READWRITE.

Функция пытается вернуть физическую память по указанному адресу addr, но не более чем maxSize байт. Результат функции – адрес возвращаемого блока физической памяти. Если физическая память не была возвращена, функция возвратит значение nil в поле addr блока памяти. В случае ошибки устанавливает переменную heapErrorCode в значение cDecommitErr.

Администратор выделенной памяти

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

В листинге 4 приведены все объявления, относящиеся к данному администратору.

Администратор выделенной памяти содержит один двунаправленный список decommittedRoot, в котором содержится информация обо всей зарезервированной, но не выделенной памяти. Первоначально список не содержит элементов. Память добавляется и удаляется из списка decommittedRoot при помощи вызовов MergeBlockAfter и RemoveBlock, таким образом, в этом списке не может содержаться двух смежных фрагментов памяти (т. е. это список второго типа). Кроме этого, в работе администратора используется одна константа – cCommitAlign, которая показывает, по какой границе будут выравниваться запросы на выделение памяти.

Расскажем немного о взаимодействии между администраторами зарезервированной памяти и выделенной памяти. Для всех фрагментов памяти, которые входят в список decommittedRoot, администратор выделенной памяти производит передачу физической памяти, пользуясь функцией Commit администратора зарезервированной памяти. Если же для удовлетворения запроса администратору зарезервированной памяти не хватает зарезервированной, но не выделенной памяти, он вызывает GetSpace. Соответственно администратор выделенной памяти возвращает физическую память при помощи вызова функции Decommit администратора зарезервированной памяти. После этого указанный фрагмент добавляется к списку decommittedRoot, и производится контрольная проверка на снятие резервирования с нового фрагмента памяти, образованного слиянием освобождаемого блока памяти с теми, которые уже находились в списке decommittedRoot.

Следует обратить внимание на одну особенность поведения функции GetCommittedAt администратора выделенной памяти. Данная функция в цикле вызывает функцию GetSpaceAt до тех пор, пока размер зарезервированного пространства не превысит запрашиваемый размер (см. особенности функции GetSpaceAt). Но при этом данному фрагменту памяти будет соответствовать несколько описателей в списке spaceRoot. Для чего это было сделано? Вспомним, что вызов функции VirtualAlloc для освобождения страницы (флаг MEM_RELEASE) должен в точности соответствовать вызову VirtualAlloc, который резервировал память. Таким образом, при очередном уменьшении размера блока (или при его освобождении), менеджер памяти будет в состоянии вернуть системе хотя бы часть зарезервированных фрагментов, в которых располагался этот блок памяти.

Коротко опишем каждую из функций администратора выделенной памяти.

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

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

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

Администратор используемой памяти

Алгоритм работы этого администратора, с одной стороны, является самым запутанным из всех, но, с другой стороны, и самым интересным. Он хранит информацию обо всей выделенной памяти и непосредственно общается с самим приложением Delphi, удовлетворяя его запросы. Для начала опишем все структуры данных, которые используются в нем. Все константы, типы и переменные, которые используются администратором выделенной памяти, представлены в листинге 5.

В выделенных фрагментах памяти блоки идут один за другим. Всего различают три типа блоков. Это используемые блоки, неиспользуемые блоки и заполнители. Администратор памяти оперирует с блоками, размеры которых выровнены по границе cAlign байт. Размер блока хранится в самом блоке памяти. Поскольку в настоящее время cAlign равен четырем, то, очевидно, два младших бита несущественны и могут использоваться как флаги. Третий флаг возникает при использовании самого старшего бита, что ограничивает размеры выделяемых блоков двумя гигабайтами. Формат размера приведен на рисунке 4.

Размер блока также включает в себя всю служебную информацию, которая хранится в блоке (например, если мы выделяем 99 байт памяти, то полученный размер блока будет равен 104 байтам, из них 1 байт уйдет на выравнивание и 4 байта на значение размера и флаги).

Опишем назначение каждого из флагов. Флаг cThisUsedFlag показывает, что данный фрагмент памяти используется приложением Delphi. Флаг cPrevFreeFlag указывает, что предыдущий фрагмент памяти свободен. Флаг cFillerFlag указывает на то, что данный фрагмент памяти является заполнителем.

Используемые блоки – это фрагменты памяти, выделенные приложением Delphi. Они имеют формат, показанный на рисунке 5. Мы видим, что четыре байта памяти, расположенные по отрицательному смещению от указателя, который вернула функция GetMem, являются размером блока (не забывайте, что размер блока содержит еще и флаги), остальная же его часть целиком и полностью находится в ведении приложения. Служебная часть используемого блока памяти описана в структуре TUsed.

Неиспользуемые фрагменты имеют формат, показанный на рисунке 6. Несмотря на то, что сумма всех составляющих блока равна 16 байтам, минимальный размер блока может быть равен (!) 12 байтам. В этом случае поле size1 накладывается на поле size2, и никакого искажения информации не происходит. Поскольку любой используемый блок памяти может стать впоследствии неиспользуемым, минимальный размер блока, выделенного менеджером памяти Delphi, равен 12 (или, с точки зрения приложения, 8) байтам.

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

  1. Данный блок не является заполнителем (о них ниже).
  2. Данный блок не является используемым.
  3. Предыдущий фрагмент занят (что вполне естественно, так как в противном случае, как мы увидим позже, менеджер памяти Delphi объединил бы указанные свободные блоки).

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

Служебная часть неиспользуемого блока памяти, расположенная в начале блока, описана в структуре TFree. Служебная часть неиспользуемого блока памяти, расположенная в конце блока, описана в структуре TUsed.

Теперь о заполнителях и щелях. Виртуальное адресное пространство, не имеющее под собой физической памяти, и граничащее с выделенными менеджером памяти фрагментами, называется щелью (gap). Заполнитель – это специфический блок памяти, который располагается либо сразу после, либо непосредственно перед щелью. Выделенный фрагмент памяти обязательно заканчивается заполнителем. Таким образом, это дает возможность смело адресоваться к блоку, который расположен непосредственно за текущим, если только текущий блок не является заполнителем. Размер заполнителя – от четырех до двенадцати байт. Заполнитель имеет формат, показанный на рисунке 7. Как и в случае со свободным блоком, значения size1 и size2 могут накладываться друг на друга. Значение флагов cFillerFlag и cUsedBlock для заполнителя установлены в 1, а значение флага cPrevFree зависит от того, является ли предшествующий блок используемым. Начало и конец заполнителя описываются структурой TUsed. Работа с заполнителями организована таким образом, что всякий раз после выделения блока памяти (если он граничит с уже использующимися) происходит стирание недействительных заполнителей и, при необходимости, создание новых.

Опишем структуры памяти, входящие в состав администратора используемой памяти. Прежде всего, отметим двунаправленный кольцевой список decommittedRoot, который описывает всю выделенную память, имеющуюся в распоряжении менеджера памяти Delphi. Первоначально этот список не содержит элементов.

Массив указателей smallTab хранит указатели на двунаправленные списки, состоящие из неиспользуемых блоков небольшого размера. Наибольший размер, информация о котором хранится в этом массиве, равен cSmallSize байт, что в настоящее время составляет 4 Кб. Заметим также, что элементы массива smallTab являются только указателями на элементы двунаправленного кольцевого списка, но, в отличие от таких структур, как spaceRoot, decommittedRoot и committedRoot, не являются корневыми элементами. В случае, когда свободных элементов указанного размера нет, соответствующий указатель в массиве smallTab равен nil. В качестве связей в двунаправленном списке используются поля prev и next структуры TFree в начале неиспользуемого блока. Место под массив smallTab выделяется в локальной куче процесса. Первоначально все указатели содержат значения nil.

Переменная avail является корневым элементом двунаправленного кольцевого списка, который содержит фрагменты памяти, большие чем cSmallSize. Первоначально этот список не содержит элементов. Переменная rover (переводится как «скиталец») указывает на блок памяти, который был в последний раз добавлен к списку avail. Первоначально переменная rover указывает на avail.

Переменные curAlloc и remBytes содержат соответственно указатель на текущий выделяемый фрагмент и размер этого фрагмента. Что это за фрагмент? Всякий раз, когда администратор используемой памяти обращается к администратору выделенной памяти за новой порцией памяти, образуется довольно большой фрагмент неиспользуемой памяти. Этот фрагмент оформляется как текущий выделяемый фрагмент. При удовлетворении запросов приложения, первым делом производится поиск свободного фрагмента в точности требуемого размера. Если таковой не найден, то будет произведена попытка выделить память из текущего выделяемого фрагмента. Текущий выделяемый фрагмент может иметь и нулевую длину, если он, например, расположен непосредственно перед щелью. Текущий выделяемый фрагмент не содержится ни в одном из перечисленных выше списков. Чтобы поместить его в один из таких списков, используется функция SysFreeMem (!), т. е. данный блок первоначально оформляется как используемый программой, а затем функция SysFreeMem помещает его в один из существующих списков. Первоначально curAlloc равен nil, а remBytes – нулю.

Теперь мы в состоянии взглянуть на менеджер памяти Delphi в целом. Типичное его состояние приведено на рисунке 8. Перед тем, как рассмотреть алгоритмы выделения, перераспределения и удаления памяти, опишем назначение каждой функции, входящей в администратор используемой памяти.

Инициализирует менеджер памяти. В случае успеха возвращает True.

Освобождает всю память, занимаемую или выделенную менеджером памяти Delphi.

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

Находит элемент списка committedRoot, содержащий указанный адрес addr. В случае ошибки устанавливает переменную heapErrorCode в cBadCommittedList.

Вызывается для того, чтобы создать заполнитель перед фрагментом, не имеющим под собой физической памяти. В качестве параметров передаются: указатель на начало последнего фрагмента в блоке выделенной памяти a и его размер size. В случае необходимости вставляет новый неиспользуемый блок памяти.

Вызывается для блоков памяти, которые были оформлены менеджером памяти как используемые, чтобы поместить их в соответствующие списки свободных блоков. Выполняет коррекцию статистики занятого пространства и вызывает SysFreeMem.

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

Вызывается для уничтожения заполнителя в случае, когда выделяемый фрагмент памяти граничит снизу с уже выделенным фрагментом. В качестве параметра a передается указатель на начало вновь выделяемого фрагмента памяти (либо, что то же самое, указатель на конец ранее выделенного фрагмента). Возвращает размер неиспользуемого фрагмента памяти в конце ранее выделенного фрагмента. В случае ошибки устанавливает значение heapErrorCode в одно из следующих значений: cBadFiller1, cBadFiller2, cBadFiller3.

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

Производит попытку возвратить физическую память, но не более чем size байт. При необходимости устанавливает заполнители.

Добавляет новый свободный блок памяти, начинающийся по адресу a и занимающий size байт, в какой-либо из существующих списков в зависимости от размера блока, его местоположения и т. д.

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

Добавляет новый блок памяти b в список committedRoot. В случае «слияния» блоков, все заполнители, оказавшиеся в середине блока, уничтожаются. В случае успешного завершения возвращает True.

Запрашивает у администратора выделенной памяти фрагмент размером не менее minSize байт, после чего вызывает для него функцию MergeCommit. В случае успешного завершения возвращает True.

Запрашивает у администратора выделенной памяти фрагмент памяти размером не менее minSize байт, начинающийся по адресу addr, после чего вызывает для него функцию MergeCommit. В случае успешного завершения возвращает True.

Производит поиск наименьшего блока памяти, имеющего размер больше, чем size байт, в массиве smallTab. В случае удачного завершения возвращает указатель на начало такого блока. Если такого блока не оказалось, возвращает nil.

Вызывается функцией SysGetMem в случае, когда простые попытки выделить память потерпели неудачу. Передаваемый параметр size – размер запрашиваемого блока. Возвращает выделенный блок памяти.

Реализация функции GetMem, предоставляемая менеджером памяти Delphi. Передаваемый параметр size – размер запрашиваемого блока. Возвращает выделенный блок памяти. Подробнее о работе этой функции, а также функции TryHarder смотри в разделе «Алгоритм выделения памяти».

Реализация функции FreeMem, предоставляемая менеджером памяти Delphi. Передаваемый параметр p – указатель на освобождаемый блок памяти. В случае удачного освобождения памяти возвращает cHeapOk (нуль). В случае ошибки возвращает ее код. Подробнее о работе этой функции можно прочитать в разделе «Алгоритм освобождения памяти».

Выполняет попытку перераспределения памяти по указанному адресу p без переноса существующего блока. В случае удачного завершения (когда удалось изменить размер блока на newSize) возвращает True.

Реализация функции ReallocMem, предоставляемая менеджером памяти Delphi. Передаваемые параметры – запрашиваемый размер блока и указатель на фрагмент памяти. Возвращает указатель на новый перераспределенный фрагмент памяти (или nil, если перераспределить память не удалось). Подробнее о работе этой функции, а также о функции ResizeInPlace, рассказано в разделе «Алгоритм перераспределения памяти».

Алгоритмы


Алгоритм выделения памяти

Алгоритм выделения памяти достаточно прост – в цикле перебираются все списки в порядке приоритета, и если находится нужный фрагмент, то его и возвращают программе. Схема алгоритма выделения памяти приведена на рисунке 9. Ниже мы приведем описание всех попыток, в том порядке, в котором они производятся.

  1. Пытаемся найти в массиве smallTab блок в точности нужного размера.
  2. Пытаемся выделить память из текущего выделяемого фрагмента.
  3. Пытаемся найти блок подходящего размера в начале списка avail.
  4. Просматриваем все оставшиеся элементы в списке avail, начиная с элемента, на который указывает rover.
  5. Просматриваем массив smallTab, и, если в нем находится блок большего размера, используем его для выделения памяти.
  6. Выделяем память.
  7. Пытаемся выделить память из текущего выделяемого фрагмента. Поскольку при выделении памяти она автоматически добавляется в текущий выделяемый фрагмент, эта попытка является последней, несмотря на то, что формально (по тексту программы) стоит переход на пункт 3. Цикл repeat..until в теле функции TryHarder используется как метка оператора goto и выполняется не более одного раза.

Таким образом, при выделении памяти средствами стандартного менеджера памяти Delphi:

  1. При попытке запроса блока памяти размером до 4 Кб, если существует неиспользуемый блок в точности требуемого размера, он будет возвращен.
  2. Если существует неиспользуемый фрагмент памяти, имеющий под собой физическую память, в который может поместиться запрашиваемый блок, то нового выделения памяти не произойдет.

Алгоритм освобождения памяти

Алгоритм освобождения памяти уже не является линейным, как алгоритм выделения памяти. Однако он также не представляет большой сложности, поскольку большинство развилок связано с объединением смежных свободных блоков. Алгоритм приведен на рисунке 10.

Итак, освобожденный блок помещается в один из следующих списков:

  1. Если он граничит с текущим выделяемым фрагментом, то добавляется к нему.
  2. Если его размер меньше, чем cSmallSize, он помещается в соответствующий список из массива smallTab.
  3. Блок помещается в список avail. Указатель rover устанавливается на него.

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

Алгоритм перераспределения памяти

Алгоритм перераспределения памяти, как и следовало ожидать, работает по-разному при уменьшении и увеличении размера фрагмента. Разберем алгоритмы раздельно. Алгоритм, который используется при уменьшении размера блока, представлен на рисунке 11, а алгоритм, который используется при увеличении размера блока — на рисунке 12. Если при расширении фрагмента не удалось произвести изменения размера по месту, будет выделен новый фрагмент нужного размера, и в него будет скопировано содержимое старого блока.

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

В случае уменьшения размера блока обратим внимание на необходимость следить за тем, чтобы не образовывались неиспользуемые фрагменты памяти размера, меньшего, чем sizeof(TFree) байт. Поэтому в подобной ситуации размер блока сохраняется прежним.

В случае увеличения размера блока никаких глобальных действий (таких, как удаление свободного блока, объединения блоков) не производится, пока не выяснится, что перераспределение памяти по месту выполнимо.

Используйте ReallocMem в библиотеке DLL для указателя первого выделенный в приложении хоста (Delphi XE)

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

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

Использование ReallocMem в DLL приводит к нарушению прав доступа, может у плзло объяснить, почему?

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

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

Если вы собираете как приложение и DLL с пакетами времени выполнения включен, они могут совместно использовать один экземпляр RTL, и , таким образом , один экземпляр родного менеджера памяти в Delphi. Если пакеты времени выполнения не включены, вы можете использовать один экземпляр родного менеджера памяти Delphi, путем использования ShareMem или SimpleShareMem блока в обеих проектах. См совместного использования памяти на DocWiki Embarcadero в.

Обратите внимание , что эти подходы работают только в проектах Builder Delphi / C ++, и требуют приложения и библиотеки DLL , которые будут собраны в одной и той же версии в Delphi / C ++ Builder.

Если приложение не написано в Delphi / C ++ Builder, то вам придется прибегнуть к использованию ОС, предоставляемый API управления памятью для любой памяти, которая проходит через границу DLL, такие как:

  • LocalAlloc() / LocalReAlloc() / LocalFree()
  • CoTaskMemAlloc() / CoTaskMemRealloc() / CoTaskMemFree()
  • IMalloc интерфейс
  • и т.д

В любом случае, это просто плохой дизайн все вокруг, ИМХО. Это безопасно передавать память через границу DLL с целью чтения из него или наполняя его , но не выделять память в одном модуле , а затем перераспределить / бесплатно это в другом модуле — Период. Кто выделяет память должна быть только одна , чтобы перераспределить / освободить его. Это дает распределителю свободу решать , как он хочет (повторно) выделить его (в стеке? В куче? В пуле памяти?).

Если вы хотите DLL перераспределить память, то DLL должен быть один, чтобы выделить и освободить его. В противном случае, сделать приложение выделить, перераспределять, и освободить память, и DLL может просто заполнить перераспределена память со значениями по мере необходимости.

Теперь, с тем, что, даже если вы все же удалось перераспределить память безопасно , код , который вы показали , по- прежнему не будет компилировать, поскольку dll_func2() ожидает ссылку уаг (почему?) До , PHostInterfaceNew но вы передаете его PHostInterface вместо этого. Это приведет к ошибке «Типы фактических и формальных параметров варов должны быть одинаковыми». Вы не можете несовпадение типов , как это. Вам нужно будет удалить var ссылку и использовать тип литье под давлением , например:

Использование кучи в Delphi.

Программируя в Delphi мы постоянно явно или неявно взаимодействуем с менеджером кучи. Неявно его используют все функции или конструкции языка, требующие выделения памяти: создания объекта класса, создание динамического массива или длинной строки. Явное взаимодействие с этим механизмом происходит при использовании следующих функций Delphi: New, Dispose, GetMem, AllocMem, ReallocMem, FreeMem.

procedure New(var P: Pointer);

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

procedure Dispose(var P: Pointer);

Функция освобождает память в куче. Параметром принимает типизированный указатель возвращенный функцией New(). Используется только в паре с функцией выделения памяти New().

function summ( var1, var2: integer ): integer;var f_int : ^integer; s_int : ^integer; begin // выделим память new(f_int); new(s_int); f_int^ := var1; s_int^ := var2; Result := f_int^ + s_int^; // освободим память dispose(f_int); dispose(s_int); end;

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

procedure GetMem(var P: Pointer; Size: Integer);

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

function AllocMem(Size: Cardinal): Pointer;

Соответствует функции GetMem(), но после выделения память будет инициирована нулями. Возвращает указатель, как результат, а не как параметр, в отличие от той же GetMem().

procedure ReallocMem(var P: Pointer; Size: Integer);

Деиствие зависит от значений P и Size. P может быть пустым указателем или содержать адрес участка памяти, возвращенный функциями GetMem, AllocMem и ReallocMem.
Варианты:
( ( P = NIL) and ( Size = 0 ) ): ReallocMem ничего ни делает;

( ( P = NIL ) and ( Size <> 0 ) ): выделяет новый блок памяти и устанавливает P на его адрес. Можно использовать вместо GetMem();

( ( P <> NIL ) and ( Size = 0 ) ): освобождает память, адресуемую P. P будет установлен в NIL. Похоже на FreeMem(), но в отличае от него чистит указатель.

( ( P <> 0 ) and ( Size <> 0 ) ): перевыделяет указанный блок памяти (изменяет его размер). Существующие данные затронуты не будут, но если память увеличиться, то новое пространство будет содержать всякий мусор. Если для изменения размера не будет хватать памяти, то блок может быть перенесен на другое место в пределах кучи, P будет указывать на новое место.

procedure FreeMem(var P: Pointer[; Size: Integer]);

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

procedure useMemoryManager;type // шаблончик для доступа к памяти побайтово memcells = array[0..$7FFFFFFE] of byte; var p: pointer; i: integer; begin // выделим память GetMem(p, 100); // и инициируем ее какими-нибудь числами for i:=0 to 99 do memcells(p^)[i] := byte(i); // добавим памяти ReallocMem(p, 200); // и допишим числа for i:=0 to 99 do memcells(p^)[i+100] := byte(i+100); // … посмотрим, что получилось for i:=0 to 199 do writeln(inttostr(memcells(p^)[i])); // уберем после себя FreeMem(p);end;

Работа с менеджером памяти

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

Во-первых есть несколько глобальных переменных, управляемых менеджером.

var AllocMemCount: integer;

Эта переменная содержит количество выделенных блоков памяти. Используется для определения оставшихся блоков.
Использовать эту переменную стоит с осторожностью. Например, в статически слинкованных программах, модули будут иметь свои собственные экземпляры AllocMemCount.

var AllocMemSize: integer;

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

var HeapAllocFlags: word;

Набор флагов, устанавливающих опции для менеджера памяти. (по умолчанию – GMEM_MOVEABLE)

GMEM_FIXED Выделяет фиксированную память. Т.к. ОС не может перемещать блоки памяти в юзермод, то и нет нужды блокировать память (не может комбинироваться с GMEM_MOVEABLE)
GMEM_MOVEABLE Выделяет перемещаемую память. В юзермод блоки не могут быть перемещены, если они расположены в физической памяти, но могут перемещаться в пределах кучи.
GMEM_ZEROINIT При выделении памяти (например, функцией GetMem) все байты этой памяти будут выставлены в 0
GMEM_MODIFY Используется для изменения атрибутов выделенного блока памяти
GMEM_DDESHARE Введёны для совместимости с 16-разрядными версиями, но может использоваться для оптимизации DDE операций.
GMEM_SHARE — // —
GPTR GMEM_FIXED + GMEM_ZEROINIT
GHND GMEM_MOVEABLE + GMEM_ZEROINIT

Во-вторых, существует ряд полезных функций для управления менеджером.

function GetHeapStatus(): TheapStatus;

Узнать текущее состояние диспетчера памяти.
Структура TheapStatus:

THeapStatus = record // Размер памяти в байтах которая доступна программе TotalAddrSpace: Cardinal; // Сколько памяти из TotalAddrSpace не находятся в SWAPе TotalUncommitted: Cardinal; // Сколько памяти из TotalAddrSpace находятся в SWAPе TotalCommitted: Cardinal; // Сколько всего динамической памяти выделено программе TotalAllocated: Cardinal; // Сколько памяти еще доступно для выделения (увеличивается) TotalFree: Cardinal; // Сколько памяти доступно в маленьких блоках FreeSmall: Cardinal; // Сколько памяти доступно в больших блоках // непрерывно идущие маленькие блоки могут складываться FreeBig: Cardinal; // Доступная, но еще не выделявшаяся память Unused: Cardinal; // Размер памяти используемой для нужд менеджера Overhead: Cardinal; // Внутренний статус кучи HeapErrorCode: Cardinal; end;

Если используется SharedMem, то статус относится к куче разделяемой несколькими процессами.

Последнее изменение этой страницы: 2020-02-21; Нарушение авторского права страницы

ReallocMem Procedure

The ReallocMem procedure reallocates dynamic memory.

Declaration


ReallocMem( Var , Size ) Parameters
Var [in, out] Required Variant
Size [in] Required Integer
Result None

Description

Changes the size of an allocated memory block that was allocated earlier with the GetMem procedure.

Parameters

The procedure has the following parameters:

Returns the address of the reallocated memory block.

Specifies the size of the reallocated memory block.

DelphiComponent.ru — бесплатно видеоуроки по Delphi, статьи, исходники

Процедуры и функции в Delphi

Посмотрите видеоурок по процедурам и функциям (подпрограммы):

Скачайте бесплатно видеокурс Мастер Delphi Lite прямо сейчас — в нем больше видеоуроков — СКАЧАТЬ БЕСПЛАТНО!

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

Использование процедур Write и WriteLn действительно не представляет особой сложности, поскольку они встроены в компилятор Delphi. Компилятор содержит лишь небольшое количество встроенных процедур. Большинство процедур и функций можно найти в отдельных исходных файлах, называемых модулями. Все модули Delphi имеют расширение . pas.

Прежде чем процедуру можно будет использовать в приложении, следует знать имя процедуры, модуль, в котором она объявлена, и принимаемые процедурой параметры. Имя процедуры и список параметров — составные части заголовка процедуры. Заголовок простой процедуры выглядит следующим образом:

После зарезервированного слова procedure всегда указывается имя процедуры, которым может быть любой допустимый идентификатор. В приведенном примере процедура не имеет параметров. Заголовок процедуры со списком параметров выглядит следующим образом:

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

Показать скрытое содержание
unit Unit1;
interface
implementation
end.

Используйте ReallocMem в dll для указателя, сначала выделенного в хост-приложении (Delphi XE)

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

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

Использование ReallocMem в dll приводит к нарушению доступа, может ли объяснить, почему?

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

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

Если вы скомпилируете оба приложения и DLL с включенными пакетами времени выполнения, они могут совместно использовать один экземпляр RTL и, таким образом, один экземпляр менеджера памяти Delphi. Если пакеты времени выполнения не включены, вы по-прежнему можете совместно использовать один экземпляр менеджера памяти Delphi с использованием ShareMem или SimpleShareMem в обоих проектах. См. Раздел » Обмен памятью» на DocWiki Embarcadero.

Обратите внимание, что эти подходы работают только в проектах Delphi/С++ Builder и требуют, чтобы приложение и DLL были скомпилированы в той же версии Delphi/С++ Builder.

Если приложение не написано в Delphi/С++ Builder, вам придётся прибегнуть к использованию программного обеспечения управления памятью OS для любой памяти, которая проходит через границу DLL, например:

  • LocalAlloc() / LocalReAlloc() / LocalFree()
  • CoTaskMemAlloc() / CoTaskMemRealloc() / CoTaskMemFree()
  • Интерфейс IMalloc
  • и т.д

В любом случае, это всего лишь плохая конструкция вокруг, ИМХО. Безопасно передавать память через границу DLL для чтения или заполнения, но не выделять память в одном модуле, а затем повторно использовать/освобождать ее в другом модуле — Period. Тот, кто выделяет память, должен быть единственным, кто переиздает/освободит его. Это дает распределителю свободу решать, как он хочет (пере) распределить его (в стеке? На куче? В пуле памяти?).

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

Теперь, с учетом сказанного, даже если вам удалось безопасно перераспределить память, код, который вы показали, все равно не скомпилируется, поскольку dll_func2() ожидает ссылку на var (почему?) На PHostInterfaceNew но вы передаете ей PHostInterface вместо. Это приведет к ошибке «Типы фактических и формальных параметров var должны быть одинаковыми». Вы не можете рассортировать подобные типы. Вам нужно будет удалить ссылку var и использовать литье типа, например:

Используйте ReallocMem в dll для указателя, впервые выделенного в хост-приложении (Delphi XE)

У меня есть запись, которую я хочу передать в DLL, где она может быть расширена с помощью дополнительных полей. Вот что у меня есть в основном приложении:

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

Использование ReallocMem в dll приводит к нарушению прав доступа. Не могли бы вы объяснить, почему?

1 ответ

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

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

Если вы компилируете и приложение, и DLL с включенными пакетами времени выполнения, они могут совместно использовать один экземпляр RTL и, таким образом, один экземпляр встроенного менеджера памяти Delphi. Если пакеты среды выполнения не включены, вы все равно можете использовать один экземпляр встроенного диспетчера памяти Delphi, используя ShareMem или SimpleShareMem в обоих проектах. См. Раздел « Совместное использование памяти» в DocWiki Embarcadero.

Обратите внимание, что эти подходы работают только в проектах Delphi / C ++ Builder и требуют, чтобы приложение и DLL были скомпилированы в одной и той же версии Delphi / C ++ Builder.

Если приложение написано не на Delphi / C ++ Builder, вам придется прибегнуть к использованию API управления памятью, предоставляемого ОС, для любой памяти, которая пересекает границу DLL, например:

  • LocalAlloc() / LocalReAlloc() / LocalFree()
  • CoTaskMemAlloc() / CoTaskMemRealloc() / CoTaskMemFree()
  • Интерфейс IMalloc
  • так далее

В любом случае, это просто плохой дизайн, ИМХО. Безопасно передавать память через границу DLL с целью чтения из нее или ее заполнения , но не выделяйте память в одном модуле, а затем перераспределяйте / освобождайте ее в другом модуле — Period. Тот, кто выделяет память, должен быть единственным, кто перераспределяет / освобождает ее. Это дает распределителю свободу выбора, как он хочет (пере) распределить его (в стеке, в куче, в пуле памяти).

Если вы хотите, чтобы DLL перераспределяла память, то она должна быть той, которая будет выделять и освобождать ее. В противном случае заставьте приложение распределять, перераспределять и освобождать память, и DLL может просто заполнить перераспределенную память значениями по мере необходимости.

Теперь, несмотря на это, даже если вам удалось безопасно перераспределить память, показанный вами код все равно не смог бы скомпилироваться, поскольку dll_func2() ожидает ссылку на var (почему?) На PHostInterfaceNew но вы передаете ему PHostInterface вместо. Это приведет к ошибке «Типы фактических и формальных параметров var должны быть идентичны». Вы не можете не соответствовать таким типам. Вам нужно будет удалить ссылку на var и использовать приведение типа, например:

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

12 января 2009 г.

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

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

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

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

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

Менеджер памяти — это код, несколько функций в программе, которые отвечают за управление памятью (выделение и освобождение). Все запросы программы к памяти (как-то: классы, динамические массивы, строки, нетипизированные указатели) обслуживаются менеджером памяти. В Delphi менеджер памяти представлен записью (*):

Как видите, менеджер памяти представлен функциями выделения и освобождения памяти, а также изменения размера блока памяти. Эта запись хранится во внутренней глобальной переменной 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:
В exe вы её используете. Как только закончили использовать — вы её удаляете. Вы не знаете ничего про менеджер памяти в DLL, поэтому всё, что вы можете сделать со строкой, — это передать её своему менеджеру памяти:

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

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

На обоих рисунках (очень грубо, без соблюдения масштаба, показаны далеко не все объекты и т.п.) изображены пользовательские части адресного пространства процесса (т.е. области от 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:
exe:
Итак, общий менеджер памяти позволяет автоматически выполнять правило «кто выделил — тот и освобождает». Но это не единственный способ. При невозможности использования общего менеджера памяти (например, 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:
exe:

Способ 2, DLL:
exe:

Способ 3, DLL:
exe:
Заметьте, что в примерах выше строковые данные используются только для примера. В реальном коде вместо PChar предпочтительнее использовать WideString (и тогда манипуляции с указателями и памятью будут не нужны), а методы примеров применять к другим динамическим данным (например, — массивам).

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

Общие определения (DLL + exe) — вынести в отдельный модуль:
DLL: exe:
Как видно (надеюсь) из примеров, использование интерфейсов — предпочтительный способ. Потому что всё делается само, автоматически. Вам не нужно следить за размерами (а вы можете ошибиться), вам не нужно работать с низкоуровневыми указателями (а вы можете перепутать указатели или испортить их), вам не нужно следить за вызовами функций (а вы можете что-то забыть сделать или вызвать не то) — короче, тут вообще ни о чём думать не надо. Разве не рай? :) Конечно, использование интерфейсов требует больше служебного кода со стороны передающего на классы-переходники (в данном примере — DLL, но это мог бы быть и exe, если делать передачу в обратную сторону через callback), но это с лихвой окупается удобством использования.

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

Итак, давайте просуммируем сказанное в кратком 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. Ну и как вы достучитесь до чужого менеджера памяти? Никак. Поэтому библиотека и должна предоставлять функцию освобождения памяти.

Но даже, если бы вдруг у вас на руках чудесным образом оказались бы указатели на чей-то чужой менеджер памяти, заменять им свой — крайне плохая идея. И вот почему. До того момента, как вы получили в свои руки чужой менеджер памяти и установили его, у вас уже выделена целая куча памяти. Любой блок может быть освобождён сразу после того, как вы установите новый менеджер. Но запрос-то на освобождение придёт не вашему менеджеру, а чужому! Который про этот блок памяти ничего не знает! Например:
И наоборот. Поработали, выгружаем библиотеку. А при выгрузке менеджер памяти библиотеки финализируется и освобождает всю выделенную им память:
Именно поэтому нужно чётко разграничивать: какие блоки памяти вы выделили сами, а какие — выделил вам чужой дядька. И свою память вы освобождаете сами, а чужую — отдаёте обратно дядьке на освобождение.

Да, если бы:
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.

Илон Маск рекомендует:  Что такое код cpdf_place_inline_image
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL