Как решить проблему сегментации памяти и заставить FastMM освободить память для ОС?

Примечание: 32-битное приложение, которое не планируется переводить на 64-битное.

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

Приложение часто выделяет большие одномерные и двумерные динамические массивы одиночных и упакованных записей до 4 одиночных записей. По большому счету я имею в виду 5000x5000 записей (одиночных, одиночных, одиночных, одиночных) - это нормально. Кроме того, одновременно работают 6 или 7 таких массивов. Это необходимо, поскольку на этих массивах выполняется много перекрестных вычислений, и их чтение с диска было бы настоящим убийцей производительности.

Уточнив это, я часто получаю ошибки памяти из-за этих больших динамических массивов, которые не исчезнут после их выпуска, независимо от того, установил ли я для них значение 0 или завершил их. Это, конечно, то, что FastMM делает для того, чтобы быть быстрым, я это хорошо знаю.

Я отслеживаю как выделенные блоки FastMM, так и потребляемую память (RAM + PF), используя:

function CurrentProcessMemory(AWaitForConsistentRead:boolean): Cardinal;
var
  MemCounters: TProcessMemoryCounters;
  LastRead:Cardinal;
  maxCnt:integer;
begin
  result := 0;// stupid D2010 compiler warning
  maxCnt := 0;
  repeat
    Inc(maxCnt);
    // this is a stabilization loop;
    // in tight loops, the system doesn't get
    // much chance to release allocated resources, which in turn will get falsely
    // reported by this function as still being used, resulting in a false-positive
    // memory leak report in the application.
    // so we do a tight loop here, waiting, until the application reported memory
    // gets stable.
    LastRead := result;
    MemCounters.cb := SizeOf(MemCounters);
    if GetProcessMemoryInfo(GetCurrentProcess,
        @MemCounters,
        SizeOf(MemCounters)) then
      Result := MemCounters.WorkingSetSize + MemCounters.PagefileUsage
    else
      RaiseLastOSError;
    if AWaitForConsistentRead and (LastRead <> 0) and (abs(LastRead - result)>1024) then
    begin
      sleep(60);
      application.processmessages;
    end;
  until (not AWaitForConsistentRead) or (abs(LastRead - result)<1024) or (maxCnt>1000);
  // 60 seconds wait is a bit too much
  // so if the system is that "unstable", let's just forget it.
end;

function CurrentFastMMMemory:Cardinal;
var mem:TMemoryManagerUsageSummary;
begin
  GetMemoryManagerUsageSummary(mem);
  result := mem.AllocatedBytes + mem.OverheadBytes;
end;

Я запускаю код на 64-битном компьютере, и мое максимальное потребление памяти до сбоев составляет около 3,3 - 3,4 ГБ. После этого я получаю сбои, связанные с памятью / ресурсами, в любом месте приложения. Мне потребовалось некоторое время, чтобы разобраться в использовании больших динамических массивов, которые были похоронены в какой-то сторонней библиотеке.

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

Большая проблема возникает, когда текущее использование памяти составляет 1 ГБ, а для обработки следующей операции требуется 2,5 ГБ памяти или более. Мой текущий код ограничился верхним значением 1,5 ГБ используемой памяти перед возобновлением, но в этой ситуации мне пришлось бы снизить предел до 1 ГБ, что в основном заставило бы приложение возобновлять само себя после каждой операции, и даже не это гарантировало что все будет хорошо.

Что, если для другой операции потребуется обработать больший набор данных, и для этого потребуется в общей сложности 4 ГБ или больше памяти?

Следует отметить, что я не говорю о фактических 4 ГБ в памяти, а о потребляемой памяти путем выделения огромных динамических массивов, которые ОС не получает обратно после отмены выделения и, следовательно, по-прежнему считает их потребленными, поэтому складывается.

Итак, моя следующая цель - заставить fastmm освободить всю (или хотя бы часть) памяти для ОС. Я специально нацелен здесь на огромные динамические массивы. Опять же, они находятся в сторонней библиотеке, поэтому перекодирование, которое на самом деле не входит в число лучших вариантов. Намного проще и быстрее повозиться с кодом fastmm и написать процедуру для освобождения памяти.

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

В заключение: есть ли способ заставить FastMM высвободить хотя бы некоторые из своих больших блоков в ОС? (ну, конечно, есть, собственно вопрос: кто-нибудь написал это, и если да, то поделитесь мнениями?)

Спасибо

позже редактировать:

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


person ciuly    schedule 18.12.2013    source источник
comment
Интересно, что вы упоминаете сегментацию памяти в своем вопросе, но не упоминаете фрагментацию памяти как проблему.   -  person Marcus Adams    schedule 19.12.2013
comment
Зачем ты упаковываешь пластинки? Обычно это приводит к снижению производительности из-за несоосности.   -  person David Heffernan    schedule 19.12.2013
comment
А что касается тупого предупреждения компилятора D2010, то компилятор точен. Если вы удалите result := 0, тогда LastRead := result прочитает неинициализированную переменную.   -  person David Heffernan    schedule 19.12.2013
comment
@ Маркус Адамс: в моем сценарии фрагментация памяти на самом деле не проблема. FastMM выделяет огромные блоки, затем требует больше, но вместо того, чтобы как-то повторно использовать существующие доступные, умирает по запросу. Итак, вы можете рассматривать это как фрагментацию памяти, но с очень большими фрагментами. Какого рода превышение срока фрагмента   -  person ciuly    schedule 19.12.2013
comment
@ Дэвид Хеффернан: в исходном коде, когда был сделан комментарий, не было цикла. Попробуй. Вызов RaiseLastOSError не рассматривался как исключение, и поэтому он считался допустимым путем, а функция была предупреждена / указана как примечание, возвращающее значение. (Delphi 2010). Я думаю, что это происходит и с вызовом процедуры, которая вызывает прерывание.   -  person ciuly    schedule 19.12.2013
comment
@ciuly Fragmentation - это именно то, что вам нужно. Действительно ли запросы блокируются на 380 МБ?   -  person David Heffernan    schedule 19.12.2013
comment
Я комментировал код в Q. Я понимаю, что вы имеете в виду по поводу RaiseLastOSError. Конечно, нельзя ожидать, что компилятор узнает, что там внутри. Так что я бы не назвал компилятор тупым. У меня есть совсем другой способ заткнуть компилятор. Я бы создал перегрузку RaiseLastOSError, которая принимает нетипизированный параметр var, который он игнорирует, прежде чем перенаправить вызов на реальный RaiseLastOSError. Пройдите Result, и компилятор отключится.   -  person David Heffernan    schedule 19.12.2013
comment
@David Heffernan: в приложении они варьируются от 100 МБ до 800 МБ. Я обновлю вопрос   -  person ciuly    schedule 19.12.2013
comment
@ciuly, поскольку вы планируете использовать 32 бита, вы должны явно указать это в своем вопросе. То, что вы пытались запустить на 64-битном компьютере, так же актуально, как и цвет корпуса компьютера.   -  person Free Consulting    schedule 19.12.2013
comment
@Free Консультации по тестированию 64-битной ОС действительно актуальны. Насколько я помню, в 32-битной ОС по умолчанию (без параметра / 3GB) приложение действительно может использовать только 2 ГБ памяти, а я иногда использую даже больше 3. Так что это имеет некоторую значимость.   -  person ciuly    schedule 19.12.2013
comment
@FreeConsulting LARGEADDRESSAWARE конечно же!   -  person David Heffernan    schedule 20.12.2013


Ответы (3)


Я сомневаюсь, что проблема на самом деле в FastMM. Для огромных блоков памяти FastMM не выполняет перераспределения. Ваш запрос на выделение ресурсов будет обработан прямым VirtualAlloc. И тогда освобождение VirtualFree.

Предполагается, что вы выделяете объекты размером 380 МБ в один непрерывный блок. Я подозреваю, что на самом деле у вас есть рваные двухмерные динамические массивы. И это не разовые выделения. Для инициализации рваных двумерных динамических массивов размером 5000x5000 требуется 5001 выделение памяти. Один для указателей строк и 5000 для строк. Это будут блоки FastMM среднего размера. Будет перераспределение.

Я думаю, вы слишком многого просите. По моему опыту, каждый раз, когда вам требуется более 3 ГБ памяти в 32-битном процессе, игра окончена. Фрагментация адресного пространства остановит вас, прежде чем у вас закончится память. Вы не можете надеяться, что это сработает. Переключитесь на 64-разрядную версию или используйте более умный и менее требовательный шаблон распределения. Или вам действительно нужны плотные 2D-массивы? Можете ли вы использовать разреженное хранилище?

Если таким образом вы не можете уменьшить потребность в памяти, вы можете использовать файлы с отображением памяти. Это позволит вам использовать дополнительную память, которая есть в вашей 64-битной системе. Дисковый кеш системы может быть больше 4 ГБ, поэтому ваше приложение может использовать более 4 ГБ памяти без необходимости обращаться к диску.

Вы, конечно, можете попробовать разные менеджеры памяти. Честно говоря, я не надеюсь, что это поможет. Вы могли бы написать тривиальный заменяющий менеджер памяти, который использовал бы HeapAlloc. И включите кучу с низкой фрагментацией (по умолчанию включена с Vista). Но искренне сомневаюсь, что это поможет. Боюсь, у вас не будет быстрого решения. Чтобы решить эту проблему, вы столкнетесь с более фундаментальной модификацией вашего кода.

person David Heffernan    schedule 18.12.2013
comment
@FreeConsulting Нет, если компилятор D2010 согласно коду. Или я пропустил часть вопроса, в которой говорилось, что используется 64-битный компилятор. Пожалуйста, укажите мне на это. - person David Heffernan; 19.12.2013
comment
Я не переходил на 64 и не планировал. Проблема не в том, что нужно 3 ГБ, а в том, что нужно 10 раз по 500 МБ. Вы выделяете a: массив массива записей a, b, c, d: single; конец; и делаем SetLength (a, 5000, 5000); сделать что угодно SetLength (a, 0, 0) ИЛИ Завершить (a); и вы делаете это в цикле и смотрите, что происходит. После нескольких итераций у вас заканчивается память. Назовите это сегментацией, фрагментацией или даже утечкой: в будущем этого не должно произойти. - person ciuly; 19.12.2013
comment
Здесь нет никаких проблем. Я могу без проблем сделать 1000 итераций. Я думаю, что смогу делать это вечно. Я уверен, что ваша настоящая программа делает больше. Вы действительно не можете ожидать, что адресное пространство 3 ГБ будет зарезервировано в программе 4 ГБ с большим количеством выделения / перераспределения. Это обязательно приведет к фрагментации. - person David Heffernan; 19.12.2013
comment
Ах, я пропустил этот комментарий о глупости, извините. - person Free Consulting; 19.12.2013
comment
@ Дэвид Хеффернан: да, моя программа делает больше, я думал, что этого будет достаточно в качестве тестового примера (на основе моей отладки приложения), я также вижу, что это не так. Завтра я сделаю воспроизводимое тестовое приложение (сейчас здесь почти час). - person ciuly; 19.12.2013

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

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

Чтобы увидеть преимущества подхода FastMem, представьте, что ваша память устроена следующим образом:

Каждая цифра представляет собой блок размером 100 МБ.
[0123456789012345678901234567890123456789]

Небольшие выделения обозначаются буквой "s".
Крупные выделения обозначаются заглавными буквами.
[0sssss678901GGGGFFFFEEEEDDDDCCCCBBBBAAAA]

Теперь, если вы освободите все свои большие блоки, у вас не должно возникнуть проблем с выполнением аналогичных больших распределений позже.
[0sssss6789012345678901234567890123456789]

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

[0sss4sGGGGsFFFFsEEEEsDDDDsCCCCsBBBBsAAAA]

Теперь, если вы освободите большие блоки, у вас останется:
[0sss4s6789s1234s6789s1234s6789s1234s6789]

И попытка выделить что-то больше 400мб не удастся.


Опции

  1. You may be able to tweak the FastMem settings so that all your "small" allocations are also considered small by FastMem. However, there are a few situations where this won't work:
    • Any DLLs you use that allocate memory to your application but bypass FastMem may still cause fragmentation.
    • Если вы не освободите все свои большие блоки вместе, оставшиеся могут вызвать фрагментацию, которая со временем будет ухудшаться.
  2. You could take on the task of memory management yourself.
    • Allocate one very large block e.g. 3.5GB which you keep for the entire lifetime of the application.
    • Вместо использования динамических массивов вы определяете расположение указателя, которое будет использоваться при настройке нового массива.
  3. Конечно, самой простой альтернативой было бы переход на 64-разрядную версию.
  4. You could consider alternate data structures.
    • Do you really need array lookup capability? If not, another structure that allocates in smaller chunks may suffice.
    • Даже если вам нужен поиск в массиве, рассмотрите возможность использования массива с разбивкой на страницы. Разреженные массивы - это комбинация массивов и связанных списков. Данные хранятся на страницах со связанными списками, связывающими каждую страницу.
    • Простым вариантом (поскольку вы упомянули, что ваши массивы двумерные) было бы использовать это: одно измерение формирует свой собственный массив, обеспечивающий поиск в одном из нескольких массивов для второго измерения.
  5. Что касается опции альтернативных структур данных, подумайте о том, чтобы сохранить некоторые данные на диске. Да производительность будет медленнее. Но если удастся найти эффективный механизм кеширования, то, может быть, и не так много. Лучше было бы чуть помедленнее, но не глючить.
person Disillusioned    schedule 19.12.2013

Динамические массивы подсчитываются в Delphi, поэтому они должны автоматически освобождаться, когда они больше не используются. Как и строки, они обрабатываются с помощью COW (копирование при записи) при совместном использовании / хранении в нескольких переменных / объектах. Итак, похоже, у вас есть какая-то утечка памяти / ссылок (например, объект в памяти, который все еще хранится, является ссылкой на массив). На всякий случай: вы не делаете никаких трюков с низкоуровневыми указателями, не так ли?

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

person André    schedule 19.12.2013
comment
Освобождение памяти обратно диспетчеру памяти не означает, что она затем передается системе. Диспетчеры вспомогательного распределения памяти могут удерживать память и повторно использовать ее. Также, пожалуйста, не предлагайте личную электронную почту. Этот сайт посвящен тому, чтобы делиться. - person David Heffernan; 19.12.2013