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

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

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

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

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

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

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

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

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

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

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

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

10 ответов


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

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

...

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

...

в итоге:

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

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

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

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

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


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


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

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

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

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

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

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

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

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

В основном можно реализовать высокопроизводительный сборщик мусора на основе RC для таких сред выполнения, как jvm Java и среда 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's critical resources freed at this point

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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


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

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

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

Edit:

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

Википедия имеет простое объяснение недостатки ссылок подсчитаны ГК


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

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

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

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

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

управляемая куча делится на три поколения: 0, 1 и 2. Новые объекты хранятся в gen 0. Объекты, которые не утилизируются циклом GC, продвигаются к следующему поколению. Поэтому, если новые объекты, которые находятся в gen 0, переживают цикл GC 1, то они повышаются до gen 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.
}

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

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