Почему нет ссылки на подсчет + сбор мусора в С#?

Я родом из С++, и я работаю с С# около года. Как и многие другие, я смущен тем, почему детерминированное управление ресурсами не встроено в язык. Вместо детерминированных деструкторов у нас есть шаблон размещения. Люди начинают задаваться вопросом, стоит ли распространять рак IDisposable через их код, стоит усилий.

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

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

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

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

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

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

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

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

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

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

Ответ 1

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

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

...

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

...

Вкратце:

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

Ответ 2

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

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

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

Umpteenth plug: я бы порекомендовал прочитать главу о выпуске Jeffrey Richter в GC в CLR через С#, если вы действительно заинтересованы в том, как GC работает.

Ответ 3

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

Ответ 4

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

Сводка стиля стиля С++:

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

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

Ссылка на сбор мусора

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

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

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

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

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

Детерминированное удаление ресурсов

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


@Skrymsli, это цель " используя" ключевое слово, Например:.

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 critical resources freed at this point

См. также правильно реализовать IDisposable.

Ответ 5

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

Конструкция using обеспечивает "детерминированное" управление ресурсами и встроена в язык С#. Обратите внимание, что под "детерминированным" я подразумеваю, что Dispose гарантированно вызывается перед кодом после запуска блока using. Заметим также, что это не то, что означает слово "детерминистский", но все, кажется, злоупотребляют им в этом контексте таким образом, что отстойно.

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

Сборщик мусора не требует выполнения IDisposable. Действительно, ГК совершенно не обращает на это внимания.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ИЗМЕНИТЬ: Из комментариев до сих пор это плохая идея, потому что

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

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

Edit:

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

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

Ответ 9

Число ссылок

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

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

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

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

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

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

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

Ответ 10

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

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

В других языках (С#, 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 С#, вы также можете использовать используя construct:

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

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