Каковы сложности неуправляемого программирования памяти?


24

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

Ошибки, которые устраняет GC, кажутся (по крайней мере, внешнему наблюдателю) вещами, которые программист, который хорошо знает свой язык, библиотеки, концепции, идиомы и т. Д., Не сделал бы. Но я могу ошибаться: действительно ли ручная обработка памяти сложна?


3
Пожалуйста, разверните, чтобы рассказать нам, как на ваш вопрос нет ответа в статье Википедии о сборе мусора и, более конкретно, в разделе о ее преимуществах
Яннис

Другим преимуществом является безопасность, например, переполнение буфера очень эффективно, и многие другие уязвимости безопасности возникают из-за неправильного управления памятью.
StuperUser

7
@StuperUser: Это не имеет ничего общего с происхождением памяти. Вы можете буферизовать переполненную память, которая пришла от GC, просто отлично. Тот факт, что языки GC обычно предотвращают это, является ортогональным, и языки, которые отстают от технологии GC менее чем на тридцать лет, вы сравниваете их, чтобы также предложить защиту от переполнения буфера.
DeadMG

Ответы:


29

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

Забавно, как определение «низкого уровня» меняется со временем. Когда я впервые учился программировать, любой язык, который предоставлял стандартизированную модель кучи, которая делает возможным простой шаблон размещения / освобождения, действительно считался высокоуровневым. В низкоуровневом программировании вам придется самостоятельно следить за памятью (не за выделениями, а за самими ячейками памяти!) Или писать свой собственный распределитель кучи, если вам действительно хочется.

Сказав это, на самом деле в этом нет ничего страшного или «сложного». Помнишь, когда ты был ребенком, и твоя мама сказала, чтобы ты убирал свои игрушки, когда ты закончил играть с ними, что она не твоя служанка и не собиралась убирать твою комнату для тебя? Управление памятью - это просто тот же принцип, который применяется к коду. (GC похожа на служанку, которая будет убирать за тобой, но она очень ленива и немного невежественна.) Принцип прост: каждая переменная в вашем коде имеет одного-единственного владельца, и этот владелец несет ответственность за освободите память переменной, когда она больше не нужна. ( Принцип единого владения) Для этого требуется один вызов на выделение, и существует несколько схем, которые так или иначе автоматизируют владение и очистку, поэтому вам даже не нужно записывать этот вызов в свой собственный код.

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

Свисающие ссылки: сначала обсудим этот вопрос, потому что он действительно серьезный. У вас есть два указателя на один и тот же объект. Вы освобождаете одного из них и не замечаете другого. Затем в более поздний момент вы пытаетесь прочитать (или написать, или освободить) второй. Неопределенное поведение наступает. Если вы этого не заметите, вы можете легко испортить вашу память. Предполагается, что сборка мусора делает эту проблему невозможной, гарантируя, что ничто не будет освобождено до тех пор, пока не исчезнут все ссылки на него. На полностью управляемом языке это почти работает, пока вам не придется иметь дело с внешними неуправляемыми ресурсами памяти. Затем вернемся к квадрату 1. А на неуправляемом языке все еще сложнее. (Тыкай на Мозиллу

К счастью, решение этой проблемы в основном решаемо. Вам не нужен сборщик мусора, вам нужен менеджер отладочной памяти. Я использую Delphi, например, и с одной внешней библиотекой и простой директивой компилятора я могу установить распределитель в «Режим полной отладки». Это добавляет незначительные (менее 5%) издержки производительности в обмен на включение некоторых функций, которые отслеживают используемую память. Если я освобождаю объект, он заполняет его память0x80байты (легко распознаваемые в отладчике), и если я когда-либо пытаюсь вызвать виртуальный метод (включая деструктор) для освобожденного объекта, он замечает и прерывает программу с ошибкой с тремя трассировками стека - когда объект был создан, когда он был освобожден, и где я сейчас нахожусь - плюс некоторая другая полезная информация, то возникает исключение. Это, очевидно, не подходит для сборок релизов, но делает отслеживание и исправление висячих проблем со ссылками тривиальным.

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

Вероятно, сторонники GC будут раздражены, если я попытаюсь сказать что-то подобное, поэтому позвольте мне объяснить. Помните, что определение утечки памяти касается выделенной памяти, когда она вам больше не нужна. В дополнение к отсутствию ссылок на что-либо, вы также можете утратить память, имея ненужную ссылку на нее, например, удерживая ее в объекте-контейнере, когда вы должны были ее освободить. Я видел некоторые утечки памяти, вызванные этим, и их очень трудно отследить, есть ли у вас GC или нет, так как они включают в себя совершенно правильную ссылку на память и нет явных «ошибок» для инструментов отладки, чтобы ловить. Насколько я знаю, не существует автоматизированного инструмента, который позволял бы улавливать этот тип утечки памяти.

Таким образом, сборщик мусора занимается только отсутствием ссылок утечек памяти, потому что это единственный тип, который может быть обработан в автоматическом режиме. Если бы он мог смотреть все ваши ссылки на все и освобождать каждый объект, как только на него ссылается ноль ссылок, это было бы прекрасно, по крайней мере, в отношении проблемы отсутствия ссылок. Выполнение этого в автоматическом режиме называется подсчетом ссылок, и это может быть сделано в некоторых ограниченных ситуациях, но у него есть свои проблемы, с которыми нужно иметь дело. (Например, объект A, содержащий ссылку на объект B, который содержит ссылку на объект A. В схеме подсчета ссылок ни один объект не может быть освобожден автоматически, даже если нет внешних ссылок на A или B.) сборщики мусора используют трассировкуВместо этого: начните с набора хорошо известных объектов, найдите все объекты, на которые они ссылаются, найдите все объекты, на которые они ссылаются, и т. д. рекурсивно, пока не найдете все. Все, что не найдено в процессе отслеживания, является мусором и может быть выброшено. (Для успешного выполнения этого, конечно, требуется управляемый язык, который накладывает определенные ограничения на систему типов, чтобы гарантировать, что отслеживающий сборщик мусора всегда сможет определить разницу между ссылкой и некоторой случайной частью памяти, которая выглядит как указатель.)

Есть две проблемы с трассировкой. Во-первых, это медленно, и пока это происходит, программа должна быть более или менее приостановлена, чтобы избежать условий гонки. Это может привести к заметным сбоям в работе, когда предполагается, что программа взаимодействует с пользователем, или к снижению производительности в серверном приложении. Это может быть смягчено различными методами, такими как разделение выделенной памяти на «поколения» по принципу: если выделение не будет получено при первой попытке, оно может остаться на некоторое время. Как .NET Framework, так и JVM используют сборочные сборщики мусора.

К сожалению, это связано со второй проблемой: память не освобождается, когда вы закончите с ней. Если трассировка не запускается сразу после того, как вы закончите с объектом, она будет держаться до следующей трассы, или даже дольше, если пройдет мимо первого поколения. Фактически, одно из лучших объяснений сборщика мусора .NET, которое я видел, объясняет, что для того, чтобы сделать процесс максимально быстрым, GC должен отложить сбор на максимально возможное время! Таким образом, проблема утечек памяти «решается» довольно странным образом, поскольку утечка максимально возможного количества памяти происходит как можно дольше! Это то, что я имею в виду, когда говорю, что GC превращает каждое выделение в утечку памяти. На самом деле, нет никакой гарантии, что какой-либо объект будет когда-либо собран.

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

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

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

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

Когда вы действительно смотрите на это, сборка мусора может или не может хорошо предотвращать висячие ссылки, и, как правило, плохо справляется с утечками памяти. Фактически, его единственным достоинством является не сборка мусора, а побочный эффект: он обеспечивает автоматизированный способ сжатия кучи. Это может предотвратить загадочную проблему (исчерпание памяти из-за фрагментации кучи), которая может убить программы, которые работают непрерывно в течение длительного времени и имеют высокую степень оттока памяти, а сжатие кучи практически невозможно без сбора мусора. Однако любой хороший распределитель памяти в наши дни использует сегменты для минимизации фрагментации, что означает, что фрагментация действительно становится проблемой только в экстремальных условиях. Для программы, в которой фрагментация кучи может быть проблемой, она ' Рекомендуется использовать компактный сборщик мусора. Но IMO в любом другом случае, использование сборки мусора является преждевременной оптимизацией, и существуют лучшие решения проблем, которые она «решает».


5
Мне нравится этот ответ - я продолжаю читать его время от времени. Не могу придумать соответствующее замечание, поэтому все, что я могу сказать, - спасибо.
VEMV

3
Я хотел бы отметить, что да, GC имеют тенденцию «протекать» память (по крайней мере, на некоторое время), но это не проблема, потому что он будет собирать память, когда распределитель памяти не может выделить память до сбора. При использовании языка, не относящегося к GC, утечка всегда остается утечкой, что означает, что вы можете фактически исчерпать память из-за слишком большого количества несобранной памяти. «сборка мусора - преждевременная оптимизация» ... GC не является оптимизацией и не была разработана с учетом этого. В противном случае хороший ответ.
Томас Эдинг

7
@ThomasEding: GC, безусловно, является оптимизацией; он оптимизирует работу программиста с минимальными затратами, за счет производительности и других показателей качества программ.
Мейсон Уилер

5
Забавно, что в какой-то момент вы указываете на баг-трекер Mozilla, потому что Mozilla пришла к совершенно другому выводу. У Firefox были и остаются проблемы с безопасностью, связанные с ошибками в управлении памятью. Обратите внимание, что речь идет не о том, как легко было исправить ошибку после ее обнаружения - обычно ущерб уже нанесен к тому времени, когда разработчики узнают об этой проблеме. Mozilla финансирует язык программирования Rust именно для того, чтобы предотвратить появление таких ошибок.

1
Хотя Rust не использует сборщик мусора, он использует подсчет ссылок в точности так, как описывает Мейсон, только с обширными проверками во время компиляции, а не с использованием отладчика для обнаружения ошибок во время выполнения ...
Шон Бертон,

13

Рассматривая технику управления памятью без сбора мусора из той же эпохи, что и сборщики мусора, используемые в современных популярных системах, таких как RAII в C ++. При таком подходе стоимость неиспользования автоматической сборки мусора минимальна, и GC создает множество собственных проблем. Таким образом, я бы предположил, что «Не много» является ответом на вашу проблему.

Помните, когда люди думают о не-GC, они думают mallocи free. Но это гигантская логическая ошибка: вы сравниваете управление ресурсами без GC в начале 1970-х с сборщиками мусора в конце 90-х. Это, очевидно, довольно несправедливое сравнение - сборщики мусора, которые использовались, когда mallocи freeбыли разработаны, были слишком медленными для запуска какой-либо значимой программы, если я правильно помню. Сравнение чего-либо из неопределенно эквивалентного периода времени, например unique_ptr, гораздо более значимо.

Сборщики мусора могут легче справляться с контрольными циклами, хотя это довольно редкий случай. Кроме того, GC могут просто «подбрасывать» код, потому что GC позаботится обо всем управлении памятью, что означает, что они могут привести к более быстрым циклам разработки.

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

Изменить: Многие из вещей, которые вы упоминаете, не имеют ничего общего с GC. Вы путаете управление памятью и объектную ориентацию. Смотрите, вот в чем дело: если вы программируете в полностью неуправляемой системе, такой как C ++, вы можете иметь столько проверок границ, сколько захотите, и классы контейнеров Standard предлагают это. Там нет ничего GC о проверке границ, например, или строгой типизации.

Проблемы, о которых вы говорите, решаются объектно-ориентированным, а не GC Происхождение массива памяти и уверенность, что вы не пишете вне его, являются ортогональными понятиями.

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


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

@DeadMG, ты знаешь, что должен делать комбинатор? Предполагается объединить. По определению комбинатор - это функция без каких-либо свободных переменных.
SK-logic

2
@ SK-logic: я мог бы реализовать его исключительно по шаблону и не иметь никаких переменных-членов. Но тогда вы не сможете переходить в замыкания, что существенно ограничивает его полезность. Хотите прийти в чат?
DeadMG

@DeadMG, определение кристально ясно. Нет свободных переменных. Я считаю любой язык «достаточно функциональным», если есть возможность определить Y-комбинатор (правильно, а не по-вашему). Большой «+» - это, если возможно определить его с помощью комбинаторов S, K и I. В противном случае язык недостаточно выразителен.
SK-logic

4
@ SK-logic: Почему бы тебе не прийти в чат , как спросил добрый модератор? Кроме того, Y-комбинатор - это Y-комбинатор, он выполняет свою работу или нет. Версия Y-комбинатора на Haskell в основном точно такая же, как и эта, просто выраженное состояние скрыто от вас.
DeadMG

11

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

Фактически, утечки памяти довольно часто встречаются в программах, написанных на языках GCed, потому что эти языки, как правило, делают программистов ленивыми и заставляют их приобретать ложное чувство безопасности, что язык всегда каким-то образом (волшебным образом) заботится о каждом объекте, который они не хочу больше думать о

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

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

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


4

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

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


1
@ Мейсон Уилер, даже C ++ реализует очень ограниченную форму замыканий. Но это не совсем правильное закрытие.
SK-logic

1
Ты не прав. Никакой сборщик мусора не может защитить вас от того, что вы не можете ссылаться на переменные стека. И это забавно - в C ++ вы также можете воспользоваться подходом «Копировать указатель на динамически размещаемую переменную, которая будет соответственно и автоматически уничтожена».
DeadMG

1
@DeadMG, разве вы не видите, что ваш код пропускает низкоуровневые сущности через любой другой уровень, который вы строите сверху?
SK-logic

1
@ SK-Logic: ОК, у нас проблема с терминологией. Каково ваше определение «реального закрытия», и что они могут сделать, чего не могут делать закрытия Дельфи? (И включение всего, что касается управления памятью, в ваше определение перемещает целевые посты. Давайте поговорим о поведении, а не деталях реализации.)
Mason Wheeler

1
@ SK-Logic: ... а у вас есть пример чего-то, что можно сделать с помощью простых нетипизированных лямбда-замыканий, которые не могут быть выполнены замыканиями Delphi?
Мейсон Уилер

2

Действительно, управление собственной памятью - это еще один потенциальный источник ошибок.

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


3
Пропущено freeне самое страшное. Раннее freeнамного более разрушительно.
Херби

2
И двойной free!
quant_dev

Хехе! Я согласился бы с обоими двумя комментариями выше. Я никогда не совершал ни одного из этих нарушений (насколько я знаю), но я понимаю, насколько ужасными могут быть последствия. Ответ от Quant_dev говорит сам за себя - ошибки в распределении и удалении памяти, как известно, трудно найти и исправить.
Дауд говорит восстановить Монику

1
Это заблуждение. Вы сравниваете «начало 1970 года» с «концом 1990 года». ГКС , которые существовали в то время , на котором mallocи freeбыл путь без GC , чтобы идти было значительно слишком медленно , чтобы быть полезными для чего - нибудь. Вы должны сравнивать это с современным подходом без GC, таким как RAII.
DeadMG

2
@DeadMG RAII - это не ручное управление памятью
quant_dev

2

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


1

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

Единственное существенное преимущество для GC, о котором я знаю, - это то, что вы можете освободить объект в своей программе и знать, что он исчезнет, ​​когда все с ним покончат. Вы можете передать его методу другого класса и не беспокоиться об этом. Вам не важно, к каким другим методам они передаются или какие классы ссылаются на них. (Утечки памяти являются обязанностью класса, ссылающегося на объект, а не класса, который его создал.)

Без GC вы должны отслеживать весь жизненный цикл выделенной памяти. Каждый раз, когда вы передаете адрес вверх или вниз из подпрограммы, которая его создала, у вас появляется неконтролируемая ссылка на эту память. В старые добрые времена, даже с одним потоком, рекурсия и изнуренная операционная система (Windows NT) не позволяли мне контролировать доступ к выделенной памяти. Мне пришлось установить бесплатный метод в моей собственной системе распределения, чтобы некоторое время хранить блоки памяти, пока все ссылки не будут удалены. Время выдержки было чисто догадкой, но это сработало.

Так что это единственное преимущество GC, о котором я знаю, но я не мог жить без него. Я не думаю, что любой ООП будет летать без него.


1
Как ни в чем не бывало, Delphi и C ++ были довольно успешны как языки ООП без какого-либо GC. Все, что вам нужно для предотвращения «неконтролируемых ссылок» - это немного дисциплины. Если вы понимаете принцип единоличного владения (см. Мой ответ), проблемы, о которых вы здесь говорите, становятся совершенно несущественными.
Мейсон Уилер

@MasonWheeler: когда приходит время освободить объект владельца, ему нужно знать все места, на которые ссылаются его собственные объекты. Сохранение этой информации и использование ее для удаления ссылок выглядит для меня как огромная работа. Я часто обнаруживал, что ссылки еще не могут быть очищены. Мне пришлось пометить владельца как удаленного, а затем периодически возвращать его к жизни, чтобы посмотреть, сможет ли он безопасно освободиться. Я никогда не использовал Delphi, но за небольшую жертву в эффективности выполнения C # / Java дал мне большой прирост времени разработки по сравнению с C ++. (Не все из-за GC, но это помогло.)
RalphChapin

1

Физические Утечки

Ошибки, которые устраняет GC, кажутся (по крайней мере, внешнему наблюдателю) вещами, которые программист, который хорошо знает свой язык, библиотеки, концепции, идиомы и т. Д., Не сделал бы. Но я могу ошибаться: действительно ли ручная обработка памяти сложна?

Исходя из конца C, который делает управление памятью как можно более ручным и четким, чтобы мы сравнивали крайности (C ++ в основном автоматизирует управление памятью без GC), я бы сказал «не совсем» в смысле сравнения с GC, когда оно приходит к утечкам . Начинающий, а иногда и профессионал может забыть написать freeдля данного malloc. Это определенно происходит.

Тем не менее, существуют такие инструменты, как valgrindобнаружение утечек, которые сразу же обнаруживают при выполнении кода, когда / где такие ошибки происходят вплоть до точной строки кода. Когда это интегрировано в CI, становится практически невозможно объединить такие ошибки и легко исправить их. Так что это никогда не имеет большого значения в любой команде / процессе с разумными стандартами.

Конечно, могут быть некоторые экзотические случаи выполнения, которые находятся под радаром тестирования, когда их freeне удалось вызвать, возможно, при обнаружении скрытой внешней ошибки ввода, такой как поврежденный файл, в этом случае система может пропустить 32 байта или что-то в этом роде. Я думаю, что это, безусловно, может произойти даже при довольно хороших стандартах тестирования и средствах обнаружения утечек, но было бы не так критично утекать немного памяти в то, что почти никогда не происходит. Мы увидим гораздо более серьезную проблему, когда мы можем утечь огромные ресурсы даже в общих путях выполнения ниже, так как GC не сможет это предотвратить.

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

Висячие указатели

Реальная проблема с более ручными формами управления памятью не утечка для меня. Сколько собственных приложений, написанных на C или C ++, мы знаем о том, что они действительно негерметичны? Является ли ядро ​​Linux негерметичным? MySQL? CryEngine 3? Цифровые аудио рабочие станции и синтезаторы? Утечка Java VM (это реализовано в нативном коде)? Photoshop?

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

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

Когда мы пытаемся получить доступ к пуанте этих висячих указателей, мы в конечном итоге сталкиваемся с неопределенным поведением, хотя почти всегда нарушение segfault / access приводит к серьезному, немедленному падению.

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

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

Управление ресурсами: Сборка мусора

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

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

введите описание изображения здесь

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

введите описание изображения здесь

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

введите описание изображения здесь

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

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

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

Управление ресурсами: руководство

Теперь давайте рассмотрим альтернативу, где мы используем указатели на Джо и ручное управление памятью, например так:

введите описание изображения здесь

Эти синие ссылки не управляют жизнью Джо. Если мы хотим удалить его с лица земли, мы вручную просим его уничтожить, вот так:

введите описание изображения здесь

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

введите описание изображения здесь

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

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

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

Реальный мир

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

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

Крушение против утечки

Теперь какой из них хуже? Немедленное падение или тихая утечка памяти, когда Джо просто таинственно задерживается?

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

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

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

Слабые ссылки

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

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

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

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

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


-1

Вот список проблем, с которыми сталкиваются программисты C ++ при работе с памятью:

  1. Проблема с областью видимости возникает в выделенной памяти стека: ее время жизни не распространяется за пределы функции, в которой она была выделена. Существует три основных решения этой проблемы: куча памяти и перемещение точки выделения вверх в стеке вызовов или выделение изнутри объектов ,
  2. Проблема sizeof заключается в выделении стека и выделении изнутри объекта и частично выделенной памяти кучи: размер блока памяти не может изменяться во время выполнения. Решения - это массивы динамической памяти, указатели, библиотеки и контейнеры.
  3. Проблема порядка определения заключается в размещении изнутри объектов: классы внутри программы должны быть в правильном порядке. Решения ограничивают зависимости деревом и переупорядочивают классы, не используя предварительные объявления, а также указатели и кучную память и используя предварительные объявления.
  4. Внутренняя-внешняя проблема в выделенной памяти объекта. Доступ к памяти внутри объектов разделен на две части, часть памяти находится внутри объекта, а другая память находится за его пределами, и программисты должны правильно выбрать использование либо композиции, либо ссылок на основе этого решения. Решения делают решение правильно, или указатели и куча памяти.
  5. Проблема рекурсивных объектов в выделенной памяти объекта. Размер объектов становится бесконечным, если один и тот же объект помещается внутрь себя, а решения - это ссылки, куча памяти и указатели.
  6. Проблема отслеживания владения заключается в выделенной памяти кучи, указатель, содержащий адрес выделенной памяти кучи, должен быть передан из точки выделения в точку освобождения. Решения - выделенная память стека, объектно-распределенная память, auto_ptr, shared_ptr, unique_ptr, stdlib контейнеры.
  7. Проблема дублирования владения заключается в выделенной памяти кучи: освобождение может быть сделано только один раз. Решения - выделенная память стека, выделенная объектная память, контейнеры auto_ptr, shared_ptr, unique_ptr, stdlib.
  8. Проблема с нулевым указателем в выделенной памяти кучи: указатели могут иметь значение NULL, что приводит к сбою многих операций во время выполнения. Решения включают стековую память, объектно-распределенную память и тщательный анализ областей кучи и ссылок.
  9. Проблема с утечкой памяти связана с выделением памяти в куче: забываем вызывать delete для каждого выделенного блока памяти. Решения - это такие инструменты, как valgrind.
  10. Проблема переполнения стека связана с рекурсивными вызовами функций, которые используют стековую память. Обычно размер стека полностью определяется во время компиляции, за исключением случая рекурсивных алгоритмов. Неправильное определение размера стека ОС также часто вызывает эту проблему, поскольку нет способа измерить требуемый размер стека.

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


1
-1: не ответ на вопрос.
Sjoerd
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.