Почему нет подсчета ссылок + сборка мусора в C #? - PullRequest
54 голосов
/ 15 мая 2009

Я родом из C ++ и работаю с C # около года. Как и многие другие, я не понимаю, почему детерминистическое управление ресурсами не является встроенным в язык. Вместо детерминированных деструкторов мы имеем образец распоряжения. Люди начинают задумываться стоит ли распространение идентифицируемого рака через их код.

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

Что если C # был изменен так, что:

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

  • Почему это плохая идея?
  • Будет ли это побеждать цель сборщика мусора?
  • Возможно ли реализовать такую ​​вещь?

EDIT: Из комментариев пока это плохая идея, потому что

  1. GC быстрее без подсчета ссылок
  2. проблема обращения с циклами в графе объектов

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

Так что оптимизация скорости перевешивает минусы, которые вы:

  1. может не освободить ресурс без памяти своевременно
  2. может освободить ресурс без памяти слишком рано

Если ваш механизм очистки ресурсов детерминирован и встроен в язык, вы можете исключить эти возможности.

Ответы [ 10 ]

49 голосов
/ 26 мая 2009

Брэд Абрамс опубликовал электронное письмо от Брайана Гарри , написанное во время разработки .Net framework. В нем подробно описываются многие причины, по которым подсчет ссылок не использовался, даже когда одним из ранних приоритетов было сохранение семантической эквивалентности с VB6, который использует подсчет ссылок. В нем рассматриваются такие возможности, как подсчет ссылок некоторых типов, а не других (IRefCounted!), Или подсчет ссылок конкретных экземпляров, и почему ни одно из этих решений не было сочтено приемлемым.

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

...

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

...

В итоге:

  • Мы считаем, что очень важно решить проблему с циклом не заставляя программистов понять, выследить и дизайн вокруг этой сложной структуры данных проблемы.
  • Мы хотим убедиться, что у нас высокая производительность (как скорость, так и рабочий набор) система и наш анализ показывает, что с использованием подсчета ссылок для каждого объекта в системе не позволит нам достичь этого цель .
  • По разным причинам, включая состав и кастинг проблемы, нет простого прозрачного Решение иметь только эти объекты что нужно, это ref count .
  • Мы решили не выбирать решение, которое обеспечивает детерминированный завершение для одного язык / контекст, потому что это тормозит Взаимодействие с другими языками и вызывает раздвоение библиотек классов создавая определенный язык версии.
30 голосов
/ 15 мая 2009

Сборщик мусора не требует написания метода Dispose для каждого класса / типа, который вы определяете. Вы определяете только один, когда вам нужно явно что-то сделать для очистки; когда вы явно выделяете собственные ресурсы. В большинстве случаев GC просто восстанавливает память, даже если вы делаете что-то вроде new () для объекта.

GC выполняет подсчет ссылок - однако он делает это по-другому, находя, какие объекты «достижимы» (Ref Count > 0) каждый раз, когда он делает коллекцию ... он просто не делает сделать это целочисленным способом. , Недоступные объекты собраны (Ref Count = 0). Таким образом, среда выполнения не должна выполнять ведение / обновление таблиц каждый раз, когда объект назначается или освобождается ... должно быть быстрее.

Единственное существенное различие между C ++ (детерминированным) и C # (недетерминированным) состоит в том, что объект будет очищен. Вы не можете предсказать точный момент, когда объект будет собран в C #.

Еще один плагин: я бы рекомендовал прочитать отдельную главу Джеффри Рихтера о GC в CLR через C # , если вам действительно интересно, как работает GC.

21 голосов
/ 15 мая 2009

Подсчет ссылок был опробован в C #. Я считаю, что люди, которые выпустили Rotor (эталонную реализацию CLR, для которой был сделан доступ к источнику), сделали GC на основе подсчета ссылок, просто чтобы посмотреть, как он будет сравниваться с поколением. Результат был удивительным - «сток» GC был намного быстрее, это было даже не смешно. Я не помню точно, где я услышал это, я думаю, что это был один из подкастов Hanselmuntes. Если вы хотите, чтобы C ++ был в значительной степени раздавлен сравнением производительности с C # - приложение китайского словаря Google Рэймонда Чена. Он сделал версию на C ++, а затем Рико Мариани сделал версию на C #. Я думаю, что Raymond 6 потребовалось несколько итераций, чтобы окончательно победить в версии C #, но к тому времени ему пришлось отбросить все приятные объектно-ориентированные C ++ и перейти на уровень Win32 API. Все это превратилось в хак производительности. Программа C #, в то же время, была оптимизирована только один раз, и в итоге все равно выглядела как приличный ОО проект

14 голосов
/ 26 мая 2009

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

Подсчет ссылок в стиле C ++:

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

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

Подсчет ссылок на сборку мусора

  • Отложенный RC: Изменения счетчика ссылок на объекты игнорируются для стековых и регистровых ссылок. Вместо этого при запуске GC эти объекты сохраняются путем сбора корневого набора. Изменения счетчика ссылок могут быть отложены и обработаны партиями. Это приводит к более высокой пропускной способности .

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

  • Обнаружение цикла: для полной реализации ГХ также должен использоваться детектор цикла. Однако можно выполнять обнаружение цикла в инкрементном режиме, что в свою очередь означает ограниченное время GC.

По сути, можно реализовать высокопроизводительный сборщик мусора на основе RC для сред выполнения, таких как Java JVM и среда выполнения .net CLR.

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

Детерминированная утилизация ресурсов

Это в значительной степени отдельная проблема. Среда выполнения .net делает это возможным с помощью интерфейса IDisposable, пример ниже. Мне также нравится ответ Гишу .


@ Skrymsli , это цель ключевого слова " using ". E.g.:

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // No need to call finalizer now
    }

    protected virtual void Dispose(bool disposing) { }
}

Затем добавить класс с критическим ресурсом:

public class ComFileCritical : BaseCriticalResource {

    private IntPtr nativeResource;

    protected override Dispose(bool disposing) {
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

Тогда использовать его так же просто, как:

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point

См. Также , правильно реализующих IDisposable .

6 голосов
/ 19 декабря 2014

Я родом из C ++ и проработал с C # около года. Как и многие другие, я не могу понять, почему детерминистическое управление ресурсами не является встроенным в язык.

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

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

Сборщик мусора не требует от вас реализации IDisposable. Действительно, GC совершенно не замечает этого.

Правда, я не очень умен ... поэтому я спрашиваю это просто из желания лучше понять, почему вещи такие, какие они есть.

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

Что если C # был изменен так, что:

Объекты подсчитываются. Когда счетчик ссылок объекта становится равным нулю, метод очистки ресурса вызывается для объекта детерминистически,

Рассмотрим объект, совместно используемый двумя потоками. Потоки стремятся уменьшить счетчик ссылок до нуля. Один поток выиграет гонку, а другой будет отвечать за очистку. Это недетерминировано. Вера в то, что подсчет ссылок по своей природе является детерминированным, является мифом.

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

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

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

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

Почему это плохая идея?

Наивный подсчет ссылок очень медленный и имеет утечки циклов. Например, Boost shared_ptr в C ++ в 10 раз медленнее, чем трассировка OCaml GC . Даже наивный подсчет ссылок на основе контекста не является детерминированным в присутствии многопоточных программ (а это почти все современные программы).

Будет ли это побеждать цель сборщика мусора?

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

Возможно ли реализовать такую ​​вещь?

Абсолютно. Ранние прототипы .NET и JVM использовали подсчет ссылок. Они также нашли, что это высосано и отбросило это в пользу отслеживания GC.

РЕДАКТИРОВАТЬ: Из комментариев пока, это плохая идея, потому что

GC быстрее без подсчета ссылок

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

проблема обращения с циклами в графе объектов

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

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

Называть слабые ссылки «легким» - это победа надежды над реальностью. Это кошмар. Они не только непредсказуемы и сложны в разработке, но и загрязняют API.

Так что оптимизация скорости перевешивает минусы, которые вы:

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

Не using своевременно освобождает ресурсы без памяти?

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

Конструкция using является детерминированной и встроена в язык.

Я думаю, что вы действительно хотите задать вопрос, почему IDisposable не использует подсчет ссылок. Мой ответ анекдотичен: я использую языки сборки мусора в течение 18 лет, и мне никогда не приходилось прибегать к подсчету ссылок. Следовательно, я предпочитаю более простые API, которые не загрязнены случайной сложностью, такие как слабые ссылки.

5 голосов
/ 15 мая 2009

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

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

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

4 голосов
/ 15 мая 2009

Здесь много проблем. Прежде всего необходимо различать освобождение управляемой памяти и очистку других ресурсов. Первый может быть очень быстрым, тогда как последний может быть очень медленным. В .NET они разделены, что позволяет быстрее очищать управляемую память. Это также подразумевает, что вы должны реализовывать Dispose / Finalizer только тогда, когда у вас есть что-то помимо управляемой памяти для очистки.

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

1 голос
/ 03 сентября 2013

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

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

В других языках (C #, Java, Python, Ruby, Erlang, ...) вместо этого вы можете использовать try-finally (или try-catch-finally), чтобы гарантировать, что код очистки всегда будет выполняться.

// Initialize some resource.
try {
    // Use the resource.
}
finally {
    // Clean-up.
    // This code will always run, whether there was an exception or not.
}

I C #, вы также можете использовать используя конструкцию:

using (Foo foo = new Foo()) {
    // Do something with foo.
}
// foo.Dispose() will be called afterwards, even if there
// was an exception.

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

1 голос
/ 15 мая 2009

Счетчик ссылок

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

Сборка мусора в .NET

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

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

Управляемая куча делится на три поколения: 0, 1 и 2. Новые объекты хранятся в 0 поколении. Объекты, которые не возвращаются циклом GC, переходят в следующее поколение. Так что, если более новые объекты, которые находятся в поколении 0, выживают в цикле 1 GC, то они переходят в поколение 1. Те ​​из них, которые выживают в цикле 2 GC, переходят в поколение 2. Поскольку сборщик мусора поддерживает только три поколения, объекты в поколении 2 пережить коллекцию остаются во втором поколении, пока они не будут определены как недоступные в будущей коллекции.

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

Таким образом, GC более эффективен, чем счетчик ссылок.

1 голос
/ 15 мая 2009

Объект, реализующий IDisposable, должен также реализовывать финализатор, вызываемый GC, когда пользователь не вызывает явный вызов Dispose - см. IDisposable.Dispose в MSDN .

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

Таким образом, ваше предложение ничего не изменит с точки зрения IDisposable.

Edit:

Извините. Не правильно прочитал ваше предложение. : - (

В Википедии есть простое объяснение недостатков подсчитанных ссылок. GC

...