TMemoryStream не освобождает память!?

Вопросы программирования на Free Pascal, использования компилятора и утилит.

Модератор: Модераторы

TMemoryStream не освобождает память!?

Сообщение Сергей Смирнов » 31.10.2008 14:52:53

При изменении размера TMemoryStream вытворяет совершенно жуткие вещи. Вот пример. Создаём проект, кидаем на форму кнопку и в её обработчик пишем следующее:

Код: Выделить всё
procedure TForm1.Button1Click(Sender: TObject);
var
  m:array of TMemoryStream;
  i:integer;

begin
  setlength(m,10);
  for i:=0 to length(m)-1 do
  begin
    m[i]:=TMemoryStream.Create;
  end;

  for i:=0 to 100 do
    m[random(length(m))].Size:=Random(1000)*10000;

  Application.MessageBox('Created','',0);
  for i:=0 to length(m)-1 do
    m[i].free;
end;


Теперь запускаем эту прогу и жмём кнопку, наблюдая в диспетчере задач за размером памяти, который она отжирает.
Мало того, что сразу выделяется не около 50Мб, а все 100-150, так ещё они и не освобождаются полностью, хотя в последних строках теста стоит цикл с TMemoryStream.free. Если нажать на кнопку многократно, то память, занимаемая нашей прогой расширяется до чудовищных размеров.

В дельфи это работает корректно, т.е. память выделяется в ожидаемых объёмах и потом благополучно возвращается.
Что делаю не так?
Аватара пользователя
Сергей Смирнов
энтузиаст
 
Сообщения: 595
Зарегистрирован: 28.04.2005 13:23:25
Откуда: Москва

Re: TMemoryStream не освобождает память!?

Сообщение GrayEddy » 31.10.2008 15:25:39

Если размер выделяемой памяти сделать фиксированным, то все ОК.
Код: Выделить всё
    m[random(length(m))].Size:=10000000; //Random(1000)*10000;

Память освобождается корректно.
Проверено на 0.9.26, Win XP

Возможно, дело в Random().

Добавлено спустя 11 минут 56 секунд:
Сейчас прогнал Random() - работает корректно.
Скорее всего, плохо работает менеджер кучи в TMemoryStream при динамическом распределении памяти для итемов.
GrayEddy
постоялец
 
Сообщения: 375
Зарегистрирован: 06.05.2005 09:37:56

Re: TMemoryStream не освобождает память!?

Сообщение Сергей Смирнов » 31.10.2008 16:02:18

GrayEddy писал(а):Сейчас прогнал Random() - работает корректно.
Скорее всего, плохо работает менеджер кучи в TMemoryStream при динамическом распределении памяти для итемов.
А у меня с Random() работает криво. Когда я запускаю прогу и начинаю жать на кнопку много раз, память, потребляемая программой ПОСТОЯННО растёт с каждым нажатием кнопки, несмотря на то, что мы её полностью освобождаем с помощью free. НО!!! По завершении программы вся эта гора занятой памяти благополучно возвращается и явного memory leak не происходит. Проблема именно в том, что идёт постоянное выделение дополнительной памяти в процессе работы программы и в реальном приложении, которое работает аналогично приведённому примеру, рано или поздно случается out of memory.
Аватара пользователя
Сергей Смирнов
энтузиаст
 
Сообщения: 595
Зарегистрирован: 28.04.2005 13:23:25
Откуда: Москва

Re: TMemoryStream не освобождает память!?

Сообщение GrayEddy » 31.10.2008 18:02:40

Поправка: в случае с Random() просто проверил эту функцию, вне приведенного выше блока.
Она работает корректно и в границах диапазона, иначе
Код: Выделить всё
m[random(length(m))].Size
вылетело бы с сообщением Out of range.

Скорее всего, схема ошибки примерно такая (схематично): Для первого итема выделяется память под 100 кб, для второго - 2 кб. Это по алгоритму. На самом деле для второго выделяется 100 кб. Т.е., возможно, берется самый максимальный объем. Итого занято 200 кб. Освобождается для первого и второго - 100 + 20 = 120 кб. Образуется дыра 80 кб.
Это моё предположение о природе ошибки. Возможно, я и ошибаюсь.

С вашим сообщением согласен - это баг.
GrayEddy
постоялец
 
Сообщения: 375
Зарегистрирован: 06.05.2005 09:37:56

Re: TMemoryStream не освобождает память!?

Сообщение Сергей Смирнов » 31.10.2008 22:26:57

GrayEddy писал(а):Это моё предположение о природе ошибки. Возможно, я и ошибаюсь.

С вашим сообщением согласен - это баг.
Похоже, что это фича такая у фрипаскалевского менеджера памяти.
Аватара пользователя
Сергей Смирнов
энтузиаст
 
Сообщения: 595
Зарегистрирован: 28.04.2005 13:23:25
Откуда: Москва

Re: TMemoryStream не освобождает память!?

Сообщение Sergei I. Gorelkin » 01.11.2008 03:12:32

В Дельфи TMemoryStream забирает память у системы напрямую, минуя менеджер памяти, который в rtl.
В FPC - работает, как и все остальное, через менеджер.
Менеджер, действительно, хапает память с запасом и сразу обратно системе не возвращает - на случай, если она сразу же кому-то снова понадобится.
Но при желании его можно настроить как угодно. Или даже заменить целиком - в составе rtl он не один.
Аватара пользователя
Sergei I. Gorelkin
энтузиаст
 
Сообщения: 1406
Зарегистрирован: 24.07.2005 14:40:41
Откуда: Зеленоград

Re: TMemoryStream не освобождает память!?

Сообщение Сергей Боровков » 01.11.2008 13:59:17

Как мы раскопали с Сергеем Смирновым, менеджер памяти округляет выделяемый размер в большую сторону до кратного 64к (назовем блоком). Пусть мы создаем TMemoryStream в 40'000'000 байт. Менеджер памяти выделяет блок 40042496 байт, который делит на два куска - 40'000'016 ( там есть заголовок 16 байт+округление до 16 байт) и кусок-остаток. Далее первый кусок используется TMemoryStream'ом. Но за время использования блока программа хочет выделить какую-то мелочь (для той же отрисовки), даже меньше килобайта легко... Прикол же состоит в том, что стандартный TMemoryManager при попытке выделить память, искать начинает с куска, который _последним_ зарегистрировался как свободный. А последним в нашем случае зарегистрировался кусок от выделенных округленных 40 мегабайт (выдеял-то TMemoryManager не 40'000'016, а 40042496). Итог - когда TMemoryStream освобождаем, то выясняется, что блок, размером в 40 мегабайт в результате не освобождается, поскольку в конец к нему, в остаток, засел маленький кусочек, который не дает освободить 40 мег.
Если мы сразу после этого попробуем взять 40'000'000, то TMemoryManager честно нам предоставит наши 40'000'000, выделенные раньше. Но до этого время нам потребуется хотя бы 1 байт, то этот один байт (округленный до 16 байт+заголовок 16 байт, но это мелочи) будет выделен из куска, который последним отметился как свободный. А последним освобожденным куском у нас является... ? правильно, 40 мегабайтный блок. Этот 40 мегабайтный блок делится на две части, 32 байта (округление, заголовок) и весь остаток. И когда нам потребуется выделить еще 40 мег памяти, прога честно съест еще 40 мег от системной памяти.

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

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

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

Методы решения - выделять куски, кратные 65536, не уменьшать размер выделенной памяти, либо что-то сделать с менеджером памяти.
Сергей Боровков
новенький
 
Сообщения: 10
Зарегистрирован: 07.01.2008 02:07:14

Re: TMemoryStream не освобождает память!?

Сообщение Sergei I. Gorelkin » 01.11.2008 15:47:16

Сергей Боровков писал(а):Если мы сразу после этого попробуем взять 40'000'000, то TMemoryManager честно нам предоставит наши 40'000'000, выделенные раньше. Но до этого время нам потребуется хотя бы 1 байт, то этот один байт (округленный до 16 байт+заголовок 16 байт, но это мелочи) будет выделен из куска, который последним отметился как свободный. А последним освобожденным куском у нас является... ? правильно, 40 мегабайтный блок. Этот 40 мегабайтный блок делится на две части, 32 байта (округление, заголовок) и весь остаток. И когда нам потребуется выделить еще 40 мег памяти, прога честно съест еще 40 мег от системной памяти.


Вообще говоря, у него два типа блоков: для небольших фиксированных размеров и для больших переменных размеров. Поэтому в данном примере он сначала попытается взять 1 байт из фиксированного блока, не трогая освобожденный (который есть блок переменного размера). Но это при условии наличия такого фиксированного блока, т.е. если мы запрашивали 1 байт когда-то раньше.
То же самое и для первой части примера: после выделения 40мБайт, оставшаяся часть блока будет использована, только если запрашивается достаточно большой кусок (я не помню точной цифры, но ее можно найти в исходнике), или если кусок маленький, но выделение такого размера ранее не производилось.
Аватара пользователя
Sergei I. Gorelkin
энтузиаст
 
Сообщения: 1406
Зарегистрирован: 24.07.2005 14:40:41
Откуда: Зеленоград

Re: TMemoryStream не освобождает память!?

Сообщение Сергей Боровков » 01.11.2008 16:56:36

факт, с фиксированными блоками мы не разбирались и я забыл про них.

Написали примерно так

m:=TMemoryStream.Create;
m.size:=40000000;
showmessage('');
m.size:=10000000;
showmessage('');
m.free;
Так вот после m.size:=40000000 и до m.size:=10000000 кто-то залезал в остаток 40 мегабайтного куска (по-моему что-то для дебага, судя по стеку, сильно не разбирались) и в результате при m.free память не освобождалась.

В реальном проекте идет частая запись в TMemoryStream, а затем отсылка содержимого стримов в сеть, запись на диск. Из-за того, что при добавлении данных в него он постоянно ресайзится, копируя данные, пришлось увеличивать размер степенчато, а затем уменьшать до реально записанного размера. Вот именно из-за этого основные проблемы и имеем, судя по всему.
Сергей Боровков
новенький
 
Сообщения: 10
Зарегистрирован: 07.01.2008 02:07:14

Re: TMemoryStream не освобождает память!?

Сообщение Sergei I. Gorelkin » 01.11.2008 17:10:57

Логично, ShowMessage явно приводит к созданию и уничтожению примерно пол-тонны всяких объектов и объектиков :)

Рассмотрите вариант хранения данных не одним непрерывным куском, а поблочно. Нечто подобное классу TDynamicArray из исходников компилятора (cclasses.pas). Это сильно облегчит нагрузку на менеджер памяти, а, прикрутив еще список свободных блоков, получится свой собственный менеджер.
Аватара пользователя
Sergei I. Gorelkin
энтузиаст
 
Сообщения: 1406
Зарегистрирован: 24.07.2005 14:40:41
Откуда: Зеленоград

Re: TMemoryStream не освобождает память!?

Сообщение Сергей Боровков » 14.11.2008 00:22:13

Спасибо за идею! Сделали аналог MemoryStream, состоящий из блоков и аналог FileStream, кеширующий чтение, либо запись. В итоге и быстро работает и память особо не ест :-).
Сергей Боровков
новенький
 
Сообщения: 10
Зарегистрирован: 07.01.2008 02:07:14


Вернуться в Free Pascal Compiler

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 22

Рейтинг@Mail.ru