Re[17]: Память и .Net
От: VladD2 Российская Империя www.nemerle.org
Дата: 03.05.06 07:12
Оценка:
Здравствуйте, kedik, Вы писали:

K>А как процесс Дотнета узнает что в системе ресурсов нехватает (память в данном случае)? Есть информация или хотя бы идеи по поводу как это происходит?


Чтобы не повторяться просто процитирую свои слова из статьи GC в .NET
Автор(ы): Чистяков Влад (VladD2 )
Дата: 02.03.2006
Уже много сказано слов о том, что такое GC, чем он хорош и как лучше его применять. Но, наверно, очень многим хочется знать, как устроен конкретный GC. Данная статья открывает некоторые подробности устройчтва GC в .NET Framework.
:

Есть три причины, вызывающих запуск процесса сборки мусора:
1. При очередном выделении памяти GC замечает, что превышен размер нулевого поколения.
2. Приложение самостоятельно вызывает метод GC.Collect().
3. Нехватка памяти в ОС.
Самой частой причиной является пункт 1. Пункт 3 обычно является большой редкостью. Ну а пункт 2 зависит от программиста, но обычно тоже крайне редок. Однако в Windows-приложениях пункт 2 может быть инициирован подсистемой учета ресурсов ОС (хендлов).
Пункты 3 приводит к сборке мусора второго поколения. GC.Collect() позволяет указать, какое поколение необходимо «подмести». А вот пункт 1 заставляет GC задуматься. Ход его мыслей приблизительно следующий: «Ага! Надо проверить лимит второго поколения. Если он превышен, то собрать его. Если нет, то проверить лимит первого поколения. Если он превышен, то собрать первое поколение. Иначе собрать только нулевое поколение».


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

Нехватка памяти

Всем известно, что при нехватке физической памяти приложение начинает сбрасывать в файл подкачки (swap file) редко используемые части занятой оперативной памяти.
Происходит это, когда приложению нужно памяти больше, чем на данный момент свободно в системе. При этом ОС в первую очередь пытается сбросить свои внутренние буферы (например, кэш файловой системы). Если этой памяти не хватает, начинается «свопинг».
Если память, сброшенная в файл подкачки, действительно используется редко, то замедление получается не такое уж большое. Ведь обращение к памяти относительно локализовано, и swapping производится не часто. Однако файл подкачки – это обычный дисковый файл, а скорость работы дисковой подсистемы в тысячи раз медленнее, чем скорость оперативной памяти.
Чтобы вы могли оценить затраты, связанные с подкачкой, я создал синтетический тест, в котором сначала занимается гандикап – огромное количество памяти (под 10 миллионов объектов, каждый из которых содержит массив длиной от 1 до 128 байт), а затем, не освобождая эту память (храня ссылки на объекты в массиве), производится эмуляция стандартной работы приложения (занимаются и освобождаются объекты). В результате создается ситуация, при которой во время эмуляции стандартной работы объем занятой оперативной памяти превышает объем физической памяти, доступной в этот момент в системе (в системе было установлен гигабайт оперативной памяти и параллельно было замещено немало приложений, общий объем занимаемой ими памяти приблизительно был равен 400 мегабайтам).
Основной тест написан на C# и, естественно, занимает память в управляемой куче. Также я создал аналогичные тесты на C++, занимающие память в обычной куче Windows, и модификацию C++-теста, занимающую память с использованием библиотеки QuickHeap (http://gzip.rsdn.ru/article/cpp/QuickHeap.xml
Автор(ы): Чистяков Владислав
Дата: 26.11.2002
), ускоряющей работу с памятью в C++-приложениях за счет использования более быстрого, но более прожорливого алгоритма.
Все три теста выполняются сначала с миллионом объектов в качестве гандикапа, а затем – с 10 миллионами. В первом случае объем занимаемой памяти, хотя и велик, но не превышает объема свободной памяти в системе, в которой производилось тестирование. Во втором случае объем гандикапа приблизительно равен объему оперативной памяти в системе. Расчет делается на то, что в обоих случаях после занятия гандикапа приложение не трогает принадлежащие к нему объекты. Вместо этого оно начинает интенсивно выделять большое количество объектов, не удерживая ссылки на них в управляемом тесте, и освобождая объекты в неуправляемых тестах. Таким образом, в основной части теста работа одновременно ведется с небольшим количеством объектов и, соответственно, памяти.
Ниже приведены результаты C++-теста, использующего стандартную кучу Windows:
e:\MyProjects\Tests\Perf\CppNew2\release>CppNew2.exe
Timestamp: 4.238123 sec.
ObjCount=11001000

e:\MyProjects\Tests\Perf\CppNew2\release>CppNew2.exe
Timestamp: 37.864712 sec.
ObjCount=20001000

Значение ObjCount – количество объектов, создаваемых при работе теста, включая как гандикап, так и рабочие объекты. Как видите, второй тест сильно отстал от первого, но отставание все же уложилось в разумные рамки (порядка 10 раз).
Если последить за картиной занятия памяти, то наблюдается следующее. При запуске с миллионом объектов объем занятой виртуальной памяти быстро поднималась примерно до 100 мегабайт и держалась на этом уровне до конца теста. Объем workset-а так же быстро достигал 100 мегабайт и так же неизменно держался до окончания теста.
При запуске теста с десятью миллионами объектов картина в корне отличалась. Сначала и workset и объем виртуальной памяти быстро достигали отметки в 400+ мегабайт. Далее объем виртуальной памяти продолжал постепенно (уже не так быстро) расти, а объем workset-а начал падать и стабилизировался на отметке 10-12 мегабайт (это объем занимаемый объектами, одновременно живущими на стадии «эмуляции обычной работы приложения»).
Совершенно ясно, что если бы приложение начало во время работы обращаться к предварительно занятым объектам (тем самым 10 миллионам), то все было бы значительно печальнее, так как при этом начался бы постоянный свопинг, и основное время тратилось бы на работу с винчестером.
Ради удовлетворения любопытства я повторил тот же тест с использованием своей библиотеки QuickHeap:
e:\MyProjects\Tests\Perf\CppNew2\release>CppNew2.exe
Timestamp: 2.193539 sec.
ObjCount=11001000

e:\MyProjects\Tests\Perf\CppNew2\release>CppNew2.exe
Timestamp: 14.508904 sec.
ObjCount=20001000

Забавно, что даже в условиях свопинга она показала намного лучший результат, чем стандартная куча Windows. Забавно это потому, что QuickHeap достигает своего быстродействия за счет перерасхода памяти. Но еще забавнее было наблюдать за картиной выделения памяти.
При запуске с миллионом объектов объем занятой виртуальной памяти скакнул к ~130 мегабайтам, что существенно выше, нежели в тесте с кучей Windows, но объем workset-а вырос всего ~96 мегабайт! Таким образом, получается, что QuickHeap хотя и тратит больше памяти, но обеспечивает лучшую локализацию данных (по крайней мере, при первом обращении).
Запуск теста с 10 миллионами предварительно занятых объектов привел к очень быстрому занятию порядка гигабайта виртуальной памяти и довольно быстрому увеличению этого объема до 1.3 гигабайта (напомню, что куча Windows постепенно росла вплоть до отметки в 1 гигабайт). Затем объем виртуальной памяти стабилизировался. Workset рос плавно (но быстро) до отметки в 400 мегабайт, после чего столь же плавно (но столь же быстро) спустился до отметки 14-9 мегабайт.
Большую скорость QuickHeap я в данном случае могу объяснить именно лучшей локальностью данных, что вызвало меньшее количество циклов свопинга. Так что хотя общее количество виртуальной занятой памяти было больше, но обращений к диску (как не странно) оказалось значительно меньше.
Ну, а что же GC? Для GC подобный тест оказался самым плохим паттерном использования. Вот его результаты:
e:\MyProjects\Tests\Perf\GC2\GC2\bin\Release>GC2.exe
00:00:02.1843580
Количество созданных объектов: 11001000
Количество сборок мусора поколения 0: 590
Количество сборок мусора поколения 1: 154
Количество сборок мусора поколения 2: 7

e:\MyProjects\Tests\Perf\GC2\GC2\bin\Release>GC2.exe
00:20:47.7608425
Количество созданных объектов: 20001000
Количество сборок мусора поколения 0: 769
Количество сборок мусора поколения 1: 273
Количество сборок мусора поколения 2: 27

Да-да! Две секунды на тест с миллионом объектов и более 20 минут (!) на тест с 10 миллионами!!! Почему такая разница? Думаю, самые прозорливые уже догадались, почему это так (я запустил параллельно архиватор! – шутка). Секрет заключается в том, что при сборке мусора второго поколения GC «дотрагивается» до всех объектов кучи. А это приводит к тому, что Windows поднимает страницы виртуальной памяти, на которых размещены объекты, с диска в физическую память. Это подтверждает и картина использования памяти. Объем виртуальной памяти начинает расти и очень быстро доходит до ~900 мегабайт. Далее он изменяется очень незначительно. А вот workset начинает постоянно расти и, достигнув отметки в 900 мегабайт, сбрасывается до ~400-500 мегабайт. Далее он снова начинает расти до 900 и снова сбрасывается. Так продолжается на протяжении двадцати минут.
В принципе, это не нормально для сборщика мусора, основанного на поколениях, так как он должен был бы пропихнуть неиспользуемые в данный момент объекты во второе поколение и забыть про них. Но почему-то наличие объектов во втором поколении постоянно подталкивает GC к сборке мусора во втором поколении. Причем делает это он тем чаще, чем больше объем второго поколения. Налицо эвристика. В чем она заключается, не знаю, но учитывать это придется. Скорее всего, столь неразумное поведение GC связано с тем, что сборка мусора второго поколения запускается при нехватке памяти в системе. Получается замкнутый круг: нехватка памяти в системе провоцирует сборку мусора второго поколения, а сборка мусора второго поколения провоцирует поднятие страниц в память. На самом деле, это просчет программистов, писавших GC, так как по-хорошему реагировать нужно было бы на нехватку физической памяти.
Какой из этого можно сделать вывод? GC хорошо ведет себя, только если имеет достаточно свободной памяти. Если workset приложения начинает дергаться, то подбираясь к объему виртуальной памяти, то сбрасываясь, знайте, что, скорее всего, вы заняли слишком много лишней памяти. Большое количество сборок второго поколения также говорит о том, что профиль вашего приложения плохо совместим с эвристиками GC.

Что же делать? Пересмотреть политику создания и удержания в живых объектов. Возможно, отказаться от кэша или подгружать малозначимые данные по требованию. Еще один простой и дешевый совет – купить лишний гигабайт памяти.

Но описанный тест — это чистой воды синтетика которой в жизни не встретишь. Обычно процессы занимают памяти меньше чем физически есть в системе и работают относительно по очереди. К тому же сборка мусора в рельных приложениях освобождает не мало памяти, что устраняет проблемы. Другими словами алгоритм в обычных условиях справляется со своими обязанностями неплохо.
... << RSDN@Home 1.2.0 alpha rev. 637>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.