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

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

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

Тестовая программа:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class BigClass
{
private:
double a[10000];
};

void TestMemory() {
cout<< "Performing TestMemory"<<endl;
vector<shared_ptr<BigClass>> list;
for (int i = 0; i<10000; i++) {
shared_ptr<BigClass> p(new BigClass());
list.push_back(p);
};
};

void TestBigBlock() {
cout<< "Performing TestBigBlock"<<endl;
char* bigBlock = new char [1024*1024*1536];
delete[] bigBlock;
}

int main() {
TestBigBlock();
TestMemory();
TestBigBlock();
}

Проблема также повторяется при использовании простых указателей с циклом new / delete или malloc / free вместо shared_ptr.

Похоже, виновник в том, что после TestMemory () виртуальная память приложения остается на уровне 827125760 (независимо от того, сколько раз я ее вызываю). Как следствие, нет свободной виртуальной машины, достаточно большой, чтобы вместить 1,5 ГБ. Но я не уверен почему — так как я определенно освобождаю память, которую использовал. Это какая-то «оптимизация производительности», которую делает CRT для минимизации вызовов ОС?

Среда для Windows 7 x64 + VS2012 + 32-разрядное приложение без LAA

4

Решение

Извините за публикацию еще одного ответа, так как я не могу комментировать; Я полагаю, что многие другие действительно очень близки к ответу 🙂

Во всяком случае, виновником, скорее всего, является фрагментация адресного пространства. Я так понимаю, вы используете Visual C ++ в Windows.

Распределитель памяти времени выполнения C / C ++ (вызывается malloc или new) использует кучу Windows для выделения памяти. У менеджера кучи Windows есть оптимизация, при которой он будет удерживать блоки с определенным ограничением размера, чтобы иметь возможность их повторного использования, если приложение позже запросит блок аналогичного размера. Для больших блоков (я не могу вспомнить точное значение, но я думаю, что это около мегабайта), он будет использовать VirtualAlloc сразу.

Другие долго работающие 32-битные приложения с множеством небольших распределений также имеют эту проблему; Единственное, что заставило меня осознать эту проблему, — это MATLAB — я использовал функцию «массива ячеек», чтобы в основном выделить миллионы блоков по 300–400 байт, вызывая именно эту проблему фрагментации адресного пространства даже после их освобождения.

Обходной путь — использовать функции кучи Windows (HeapCreate () и т. Д.) Для создания частной кучи, выделить через нее память (при необходимости передавая собственный распределитель C ++ классам вашего контейнера), а затем уничтожить эту кучу, когда вы хотите обратно памяти — у этого также есть счастливый побочный эффект того, что он очень быстрый по сравнению с удалением () с миллиардом блоков в цикле.

Число рейнольдса «что остается в памяти», чтобы вызвать проблему в первую очередь: ничего не остается «в памяти» как таковое, это скорее случай, когда освобожденные блоки помечаются как свободные, но не объединяются. У менеджера кучи есть таблица / карта адресного пространства, и он не позволит вам выделить что-либо, что заставит его объединить свободное пространство в один непрерывный блок (предположительно, эвристический анализ производительности).

2

Другие решения

Это не утечка памяти. Используемая память U была выделена средой выполнения C \ C ++. Среда выполнения применяет большую часть памяти из ОС один раз, а затем каждая новая вызванная вами память будет выделяться из этой массовой памяти. при удалении одного объекта среда выполнения не сразу возвращает память ОС, она может удерживать эту память для производительности.

1

Здесь нет ничего, что указывало бы на настоящую «утечку». Модель памяти, которую вы описываете, не является неожиданной. Вот несколько моментов, которые могут помочь понять. То, что происходит, сильно зависит от ОС.

  • Программа часто имеет одну кучу, которая может быть увеличена или уменьшена в длине. Тем не менее, это одна непрерывная область памяти, поэтому изменение размера просто меняет место, где находится конец кучи. Это делает очень трудным когда-либо «возвращать» память в ОС, так как даже один маленький крошечный объект в этом пространстве предотвратит ее сжатие. В Linux вы можете найти функцию ‘brk’ (я знаю, что вы работаете в Windows, но я предполагаю, что она делает нечто подобное).

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

  • Большие блоки неиспользуемой памяти обычно не занимают много ресурсов. Если вы больше не используете память, они могут просто попасть на диск. Не думайте, что из-за того, что некоторые функции API говорят, что вы используете память, вы на самом деле расходуете значительные ресурсы.

  • API не всегда сообщают, что вы думаете. Из-за множества оптимизаций и стратегий на самом деле может оказаться невозможным определить, сколько памяти используется и / или доступно в системе в определенный момент. Если у вас нет подробных сведений об ОС, вы не будете точно знать, что означают эти значения.

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


Например, в приведенном коде:

  1. TestBigBlock выделяет и удаляет массив, предположим, что для этого используется специальный блок памяти, поэтому память возвращается в ОС
  2. TestMemory расширяет кучу для всех маленьких объектов и никогда не возвращает кучи в ОС. Здесь куча полностью доступна с точки зрения приложений, но с точки зрения ОС она назначается приложению.
  3. TestBigBlock теперь дает сбой, поскольку, хотя он будет использовать специальный блок памяти, он разделяет общее пространство памяти с кучей, и после завершения 2 его просто не остается.
1

Есть абсолютно нет утечки памяти в вашей C ++ программе. Настоящий виновник фрагментация памяти.

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

//Valgrind Report
mantosh@mantosh4u:~/practice$ valgrind ./basic
==3227== HEAP SUMMARY:
==3227==     in use at exit: 0 bytes in 0 blocks
==3227==   total heap usage: 20,017 allocs, 20,017 frees, 4,021,989,744 bytes allocated
==3227==
==3227== All heap blocks were freed -- no leaks are possible

Пожалуйста, найдите мой ответ на ваш вопрос / сомнение в первоначальном вопросе.

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

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

Вышеприведенный параграф объясняет очень хорошо о фрагментации памяти. Некоторые шаблоны распределения (такие как частое распределение и расположение сделки) могут привести к фрагментации памяти, но ее конечное влияние (т.е. распределение памяти). 1.5GBсбой) может сильно отличаться в разных системах, так как разные ОС / менеджер кучи имеют разную стратегию и реализацию.
Например, ваша программа отлично работала на моем компьютере (Linux), однако вы столкнулись с ошибкой выделения памяти.

Что касается вашего наблюдения за размером виртуальной машины, она остается постоянной: размер виртуальной машины, видимый в диспетчере задач, не прямо пропорционален нашим вызовам выделения памяти. Это в основном зависит от того, сколько байтов находится в зафиксированном состоянии. Когда вы выделяете некоторую динамическую память (используя new / malloc) и ничего не пишете / не инициализируете в этих областях памяти, она не перейдет в состояние фиксации, и, следовательно, на размер виртуальной машины не повлияет это. Размер виртуальной машины зависит от многих других факторов и немного усложняется, поэтому мы не должны полностью полагаться на это при понимании динамического распределения памяти нашей программы.

Как следствие, нет свободной виртуальной машины, достаточно большой, чтобы вместить 1,5
GB.

Да, из-за фрагментации нет смежных 1.5GB объем памяти. Следует отметить, что общая оставшаяся (свободная) память будет более 1.5GB но не в раздробленном состоянии. Следовательно, нет большой смежной памяти.

Но я не уверен почему — так как я определенно освобождаю память, которую использовал.
Это какая-то «оптимизация производительности», которую делает CRT для минимизации вызовов ОС?

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

Возможное разрешение фрагментации памяти

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

Однако в Windows-системах есть инструменты, о которых я не очень осведомлен. Но я нашел один замечательный пост SO о том, какой инструмент (для Windows) может быть полезен для самостоятельного понимания и проверки состояния фрагментации вашей программы.

https://stackoverflow.com/a/1684521/2724703

1
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector