Почему умные указатели подсчета ссылок так популярны?

52

Как я вижу, умные указатели широко используются во многих реальных проектах C ++.

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

Почему общие указатели более популярны, чем интеграция надлежащего сборщика мусора, такого как Boehm GC ? (Или вы согласны с тем, что они более популярны, чем реальные GC?)

Я знаю о двух преимуществах обычных GC перед подсчетом ссылок:

  • Обычные алгоритмы GC не имеют проблем с референтными циклами .
  • Подсчет ссылок, как правило, медленнее, чем правильный сборщик мусора.

Каковы причины использования интеллектуальных указателей для подсчета ссылок?

Миклош Гомоля
источник
6
Я бы просто добавил комментарий, что это неправильное использование по умолчанию: в большинстве случаев этого std::unique_ptrдостаточно, и поэтому он имеет нулевые накладные расходы по сравнению с необработанными указателями с точки зрения производительности во время выполнения. Используя std::shared_ptrвезде, вы также затеняли бы семантику владения, потеряв одно из основных преимуществ интеллектуальных указателей, помимо автоматического управления ресурсами - четкое понимание цели, стоящей за кодом.
Мэтт
2
Извините, но принятый ответ здесь совершенно неверен. Подсчет ссылок имеет более высокие издержки (счет вместо бита метки и более медленную производительность во время выполнения), неограниченное время паузы при уменьшении лавины и не более сложный, чем, скажем, полупространство Чейни.
Джон Харроп

Ответы:

57

Некоторые преимущества подсчета ссылок перед сборкой мусора:

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

  2. Более предсказуемое поведение. С подсчетом ссылок вы гарантированно освободите свой объект, как только исчезнет последняя ссылка на него. С другой стороны, при сборке мусора ваш объект будет освобожден «когда-нибудь», когда система обойдет его. Для оперативной памяти это обычно не является большой проблемой на настольных компьютерах или слегка загруженных серверах, но для других ресурсов (например, файловых дескрипторов) вам часто нужно их закрывать как можно скорее, чтобы избежать потенциальных конфликтов в дальнейшем.

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

  4. Стандарт. C ++ включает подсчет ссылок (через shared_ptr) и друзей в STL, что означает, что большинство программистов на C ++ знакомы с ним, и большая часть кода C ++ будет работать с ним. Однако никакого стандартного сборщика мусора в C ++ не существует, а это означает, что вы должны выбрать один и надеяться, что он хорошо подойдет для вашего случая использования - и если это не так, то это ваша проблема, а не язык.

Что касается предполагаемых недостатков подсчета ссылок - не обнаружение циклов является проблемой, но я никогда не сталкивался лично за последние десять лет с использованием подсчета ссылок. Большинство структур данных естественно ациклические, и если вы сталкиваетесь с ситуацией, когда вам нужны циклические ссылки (например, родительский указатель в узле дерева), вы можете просто использовать слабый_ptr или необработанный указатель C для «обратного направления». Пока вы знаете о потенциальной проблеме при проектировании структур данных, это не проблема.

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

Джереми Фризнер
источник
16
Наивные реализации подсчета ссылок обычно получают намного более низкую пропускную способность, чем производственные GC (30–40%), за счет задержки. Промежуток может быть закрыт оптимизацией, такой как использование меньшего количества бит для пересчета и избегание отслеживания объектов до тех пор, пока они не исчезнут - C ++ делает это естественно, если вы в основном make_sharedвозвращаетесь. Тем не менее, задержка имеет тенденцию быть большей проблемой в приложениях реального времени, но пропускная способность, как правило, более важна, поэтому отслеживание GC так широко используется. Я не буду так быстро говорить о них плохо.
Джон Перди
3
Я бы сказал: «проще»: проще с точки зрения общего объема реализации, требуемого да, но не проще для кода, который его использует : сравните, рассказывая кому-то, как использовать RC («делайте это при создании объектов и это при уничтожении их» ) как (наивно, что достаточно часто) использовать GC ('...').
AakashM
4
«С подсчетом ссылок вы гарантированно освободите свой объект, как только исчезнет последняя ссылка на него». Это распространенное заблуждение. flyingfrogblog.blogspot.co.uk/2013/10/…
Джон Харроп
4
@JonHarrop: этот пост в блоге ужасно неправильный. Вы также должны прочитать все комментарии, особенно последний.
Дедупликатор
3
@JonHarrop: Да, есть. Он не понимает, что время жизни - это полная область действия, которая подходит к закрывающей скобке. И оптимизация в F #, которая, согласно комментариям, работает только иногда, заканчивает время жизни раньше, если переменная не используется снова. Что естественно имеет свои опасности.
Дедупликатор
26

Чтобы получить хорошую производительность от GC, GC должен иметь возможность перемещать объекты в памяти. В таком языке, как C ++, где вы можете напрямую взаимодействовать с областями памяти, это практически невозможно. (Microsoft C ++ / CLR не считается, поскольку вводит новый синтаксис для указателей, управляемых GC, и, таким образом, фактически является другим языком.)

Boehm GC, хотя и является изящной идеей, на самом деле является худшим из обоих миров: вам нужен malloc (), который работает медленнее, чем хороший GC, и поэтому вы теряете детерминированное поведение распределения / освобождения без соответствующего повышения производительности поколения GC. , Кроме того, он по необходимости консервативный, поэтому он не обязательно соберет весь ваш мусор в любом случае.

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

Тем не менее, будет интересно посмотреть, как C ++ 11 становится все более популярным, начинают ли лямбды и семантики захвата вести сообщество C ++ к тем же типам проблем выделения ресурсов и времени жизни объектов, которые заставили сообщество Lisp изобретать GC в первом место.

Смотрите также мой ответ на связанный вопрос на StackOverflow .

Даниэль Приден
источник
6
RE, Boehm GC, я иногда задавался вопросом, насколько он лично отвечает за традиционное отвращение к GC среди программистов на C и C ++, просто создав плохое первое впечатление о технологии в целом.
Леушенко
@ Leushenko Хорошо сказано. В качестве примера можно привести этот вопрос, где Boehm gc называется «правильным» gc, игнорируя тот факт, что он медленный и практически гарантирует утечку. Я нашел этот вопрос, исследуя, реализовал ли кто-то прерыватель цикла в стиле python для shared_ptr, что звучит как стоящая цель для реализации c ++.
user4815162342
4

Как я вижу, умные указатели широко используются во многих реальных проектах C ++.

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

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

Это плохая идея, потому что вам все еще нужно беспокоиться о циклах.

Почему общие указатели более популярны, чем интеграция надлежащего сборщика мусора, такого как Boehm GC? (Или вы согласны с тем, что они более популярны, чем реальные GC?)

Ух ты, у тебя так много неправильного мышления:

  1. ГК Бёма не является «правильным» ГК в любом смысле этого слова. Это действительно ужасно. Это консервативно, поэтому оно протекает и неэффективно по конструкции. Смотрите: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Совместно используемые указатели объективно не так популярны, как GC, поскольку подавляющее большинство разработчиков используют языки GC и не нуждаются в общих указателях. Просто посмотрите на Java и Javascript на рынке труда по сравнению с C ++.

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

Каковы причины использования интеллектуальных указателей для подсчета ссылок?

Вы ограничены C ++, но хотели бы иметь автоматическое управление памятью.

Джон Харроп
источник
7
Хм, это вопрос с тегом c ++, который говорит о возможностях C ++. Ясно, что любые общие утверждения говорят в коде C ++, а не в целом программировании. Поэтому, однако, «объективно» сборка мусора может использоваться за пределами мира C ++, что в конечном счете не имеет отношения к рассматриваемому вопросу.
Николь Болас
2
Ваша последняя строка явно ошибочна: вы находитесь на C ++ и рады, что вам не приходится иметь дело с GC, и это задерживает освобождение ресурсов. Есть причина, по которой Apple не любит GC, и самое важное правило для языков GC: не создавайте мусор, если у вас нет свободных ресурсов или вы не можете ничего с этим поделать.
Дедупликатор
3
@JonHarrop: Итак, сравните эквивалентные небольшие программы с GC и без, которые явно не выбраны для воспроизведения с преимуществом любой из сторон. Какой из них вы ожидаете, что вам нужно больше памяти?
Дедупликатор
1
@Deduplicator: я могу предусмотреть программы, которые дают любой результат. Подсчет ссылок превосходит отслеживание GC, когда программа предназначена для хранения памяти в куче до тех пор, пока она не сохранится в детской (например, очереди списков), потому что это патологическая производительность для поколения GC и будет генерировать наиболее плавающий мусор. Трассировка сборки мусора потребует меньше памяти, чем подсчет ссылок на основе области действия, когда имеется много небольших объектов, а время жизни короткое, но статически недостаточно известно, что-то вроде логической программы, использующей чисто функциональные структуры данных.
Джон Харроп
3
@JonHarrop: я имел в виду GC (трассировка или что-то еще) и RAII, если вы говорите на C ++. Который включает подсчет ссылок, но только там, где это полезно. Или вы можете сравнить с программой Swift.
Дедупликатор
3

В MacOS X и iOS, а также у разработчиков, использующих Objective-C или Swift, подсчет ссылок популярен, потому что он обрабатывается автоматически, а использование сбора мусора значительно сократилось, поскольку Apple больше не поддерживает его (мне сказали, что приложения, использующие сборка мусора прекратится в следующей версии MacOS X, а сборка мусора никогда не была реализована в iOS). Я действительно серьезно сомневаюсь, что когда-либо было доступно много программного обеспечения, использующего сборку мусора.

Причина избавления от сборки мусора: она никогда не работала надежно в среде в стиле C, где указатели могли «убегать» в области, недоступные сборщику мусора. Apple твердо верит и считает, что подсчет ссылок происходит быстрее. (Здесь можно сделать какие-либо заявления об относительной скорости, но никто не смог убедить Apple). И, в конце концов, никто не использовал сборку мусора.

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

gnasher729
источник
Насколько я понимаю, дело было не в том, что это C-подобная среда, в которой все решается, а в том, что GC недетерминирован и требует гораздо больше памяти, чтобы иметь приемлемую производительность, а вне сервера / рабочего стола всегда немного скудно.
Дедупликатор
Отладка того, почему сборщик мусора уничтожил объект, который я все еще использовал (что привело к сбою), решила это для меня :-)
gnasher729
Ах, да, это бы тоже. Вы в конце концов выяснили почему?
Дедупликатор
Да, это была одна из многих функций Unix, где вы передаете void * как «контекст», который затем возвращается вам в функции обратного вызова; void * действительно был объектом Objective-C, и сборщик мусора не осознавал, что объект был спрятан в вызове Unix. Callback вызывается, бросает void * в Object *, kaboom!
gnasher729
2

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

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

    Следствие: любой сборщик мусора в C ++ будет пропускать объекты, которые должны быть собраны.

  • В C ++ вы можете делать арифметику указателей для получения указателей. Таким образом, если вы не найдете указатель на начало блока, это не означает, что на этот блок нельзя ссылаться.

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

    Примечание. Никакой сборщик мусора в C ++ не может обрабатывать код с помощью таких хитростей:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;

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

cmaster
источник
2
« они смешаны с другими данными. » Это не так много, что они «смешаны» с другими данными. Использовать систему типов C ++ легко, чтобы увидеть, что является указателем, а что нет. Проблема в том, что указатели часто становятся другими данными. Сокрытие указателя в целом числе - это, к сожалению, распространенный инструмент для многих API в стиле C.
Николь Болас
1
Вам даже не нужно неопределенное поведение, чтобы испортить сборщик мусора в c ++. Вы можете, например, сериализовать указатель на файл и прочитать его позже. Между тем, ваш процесс может не содержать этот указатель где-либо в своем адресном пространстве, поэтому сборщик мусора может собирать этот объект, а затем, когда вы десериализуете указатель ... К сожалению.
Bwmat
@Bwmat "Даже"? Запись указателей на такой файл кажется немного ... надуманной. В любом случае, та же самая серьезная проблема преследует указатели на объекты стека, они могут исчезнуть, когда вы читаете указатель обратно из файла в другом месте кода! Десериализация неверного значения указателя - неопределенное поведение, не делайте этого.
Гайд
Если конечно, вы должны быть осторожны, если вы делаете что-то подобное. Предполагалось, что это будет пример того, что сборщик мусора в целом не может работать «должным образом» во всех случаях в c ++ (без изменения языка)
Bwmat
1
@ gnasher729: Эмм, нет? Прошлые указатели в порядке?
Дедупликатор