РАИИ и сборщик мусора


недавно я смотрел отличный разговор Херба Саттера о " утечке свободного C++..."на CppCon 2016, где он говорил об использовании интеллектуальных указателей для реализации RAII (Resource acquisition is initialization) - концепций и о том, как они решают большинство проблем с утечкой памяти.

теперь мне было интересно. Если я строго следую правилам RAII, что, кажется, хорошо, почему это будет отличаться от наличия сборщика мусора в C++? Я знаю, что с RAII программист полностью контролирует когда ресурсы будут освобождены, но это в любом случае выгодно просто сборщик мусора? Будет ли это менее эффективно? Я даже слышал, что сборщик мусора может быть более эффективным, так как он может освобождать большие куски памяти за один раз вместо освобождения небольших фрагментов памяти по всему коду.

12 73

12 ответов:

Если я строго следую правилам RAII, что, кажется, хорошо, почему это будет отличаться от наличия сборщика мусора в C++?

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

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

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

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

в таких случаях GC не сокращает его, что является причиной В C# (например) у вас есть IDisposable интерфейс.

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

может быть ... зависит от реализации.

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

это дает ему два преимущества. Во-первых, будут определенные типы проблем, которые RAII не может решить. Это, по моему опыту, редкость.

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

недостатком является то, что без RAII управление ресурсами, срок службы которых вы хотите ограничить, сложно. Языки GC в основном сводят вас к чрезвычайно простому сроку службы с привязкой к области или требуют от вас управления ресурсами вручную, как и в C, с ручным указанием, что вы сделали с ресурсом. Их система жизненного цикла объекта сильно привязана к GC и не работает хорошо для плотного управления жизненным циклом больших сложных (но безцикловых) систем.

большинство реализаций GC также заставляет нелокальность полноценные классы; создание непрерывных буферов общих объектов или составление общих объектов в один более крупный объект-это не то, что большинство реализаций GC делают легким. С другой стороны, C# позволяет создавать тип значения structS с несколько ограниченными возможностями. В нынешнюю эпоху архитектуры ЦП, кэш дружелюбие является ключевым, и отсутствие локальности GC сил является тяжелым бременем. Поскольку эти языки имеют байт-код времени выполнения по большей части, в теория среда JIT может перемещать обычно используемые данные вместе, но чаще всего вы просто получаете равномерную потерю производительности из-за частых промахов кэша по сравнению с C++.

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

обратите внимание, что RAII это идиома программирования, в то время как GC техника управления памятью. Итак, мы сравниваем яблоки с апельсинами.

но мы можем ограничить RAII его аспектами управления памятью только и сравните это с методами GC.

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

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

во многих случаях полезно (думать std::vector<std::map<std::string,int>>) подсчет ссылок неявен (так как он может быть только 0 или 1) и практически опущено, но функции contructor и destructor (существенные для RAII) ведут себя так, как если бы был бит подсчета ссылок (который практически отсутствует). В std::shared_ptr есть подлинный счетчик ссылок. Но память все равно имплицитновручнуюnew и delete срабатывает внутри конструкторов и деструкторов), но это "неявное" delete (в деструкторах) дает иллюзию автоматического управления памятью. Однако, звонки на new и delete еще бывают (а они стоят времени).

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

некоторые алгоритмы GC (особенно generational copying garbage collector) не утруждают себя освобождением памяти для отдельные объекты, это освободить повально после копирования. На практике OCaml GC (или SBCL one) может быть быстрее, чем подлинный стиль программирования C++ RAII (для некоторые не все, вроде алгоритмов).

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

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

Я рекомендую прочитать руководство по сбору мусора.

в коде C++ вы можете использовать ГК Бема или Ravenbrook депутатов или код свой собственный трассировка сборщик мусора. Конечно, использование GC-это компромисс (есть некоторые неудобства, например, недетерминизм, отсутствие гарантий времени и т. д...).

Я не думаю, что RAII является конечным способом работы с памятью во всех случаях. В нескольких случаях кодирование вашей программы в подлинно и эффективно реализациях GC (подумайте о Ocaml или SBCL) может быть проще (для разработки) и быстрее (для выполнения), чем кодирование его с причудливым стилем RAII в C++17. В других случаях это не так. МММ.

например, если вы кодируете интерпретатор схемы в C++17 с самым причудливым стилем RAII, вам все равно нужно будет кодировать (или использовать) a явно GC внутри него (потому что куча схемы имеет циркулярности). И самое доказательство помощников кодируются на языках GC-ed, часто функциональных, (единственный, который я знаю, который закодирован в C++ - это Lean) для важные причины.

кстати, я заинтересован в поиске такой реализации схемы C++17 (но менее заинтересован в ее кодировании сам), предпочтительно с некоторой многопоточной способностью.

RAII и GC решают задачи в совершенно разных направлениях. Они совершенно разные, несмотря на то, что некоторые сказали бы.

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

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

  • C++ предлагает RAII через конструкторы/деструкторы и GC через shared_ptr (если я могу сделать аргумент, что refcounting и GC находятся в одном классе решений, потому что они оба предназначены, чтобы помочь вам не нужно обращать внимание на продолжительность жизни)
  • Python предлагает RAII через with и GC через систему refcounting плюс сборщик мусора
  • C# предлагает RAII через IDisposable и using и GC через поколенческий сборщик мусора

шаблоны появляется на каждом языке.

одна из проблем сборщики мусора, что трудно предсказать поведение программы.

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

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

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

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

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

кроме того, вы можете в основном почувствовать напряжение древнего "является ли Java или C++ лучшим языком?"флейм треск в комментариях. Интересно, как может выглядеть" приемлемый " ответ на этот вопрос, и мне любопытно это увидеть в итоге.

но один момент, возможно важный концептуальная разница еще не была указана: с RAII вы привязаны к потоку, который вызывает деструктор. Если ваше приложение является однопоточным (и даже если это был Херб Саттер, который заявил, что Бесплатный Обед Закончился: большинство программ сегодня эффективно все еще и однопоточный), то одно ядро может быть занято обработкой очистки объектов, которые являются больше не актуально для реальной программы...

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

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

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

ГКС только общаются с динамическими распределениями, сбрасывая программиста тревожиться о полном томе размещенных объектов над продолжительностью жизни программы (они должны заботиться только о пиковом параллельном распределении объема фитинга)

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

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

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

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

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

RAII и сбор мусора предназначены для решения различных проблем.

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

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

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

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

это вполне выполнимо - и, по сути, фактически сделано - с RAII (или с простым malloc/free). Видите ли, вы не обязательно всегда используете распределитель по умолчанию, который освобождает только по частям. В определенных контекстах вы используете пользовательские распределители с различными типами функциональность. Некоторые распределители имеют встроенную способность освобождать все в некоторой области распределителя, все сразу, без необходимости повторять отдельные выделенные элементы.

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