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


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

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

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

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

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

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

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

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

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

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

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

10 53

10 ответов:

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

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

...

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

...

в итоге:

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

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

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

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

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

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

Подсчет Ссылок На Стиль C++:

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

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

Сбор Мусора Подсчета Ссылок

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

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

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

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

Я думаю, сборщики трассировки частично используются по историческим причинам:многие из последних улучшений в подсчете ссылок появились после выпуска среды выполнения 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's critical resources freed at this point

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

не using освободите ресурс non-памяти своевременно?

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

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

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

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

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

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

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

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

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

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

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

Edit:

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

Википедия имеет простое объяснение недостатки ссылок засчитываются GC

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

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

вывоз мусора в .NET

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

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

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

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

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

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

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

в других языках (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.