Подтвердить что ты не робот

Почему сбор мусора, когда RAII доступен?

Я слышал, как С++ 14 представлял сборщик мусора в самой стандартной библиотеке С++. В чем смысл этой функции? Разве это не причина, по которой RAII существует в С++?

  • Как будет присутствовать стандартный сборщик мусора библиотеки в семантике RAII?
  • Как это важно для меня (программиста) или того, как я пишу программы на С++?
4b9b3361

Ответ 1

Сбор мусора и RAII полезны в разных контекстах. Наличие GC не должно влиять на использование RAII. Поскольку RAII хорошо известна, я даю два примера, где GC удобен.


Сбор мусора будет большой помощью в реализации блокировки данных.

[...] выясняется, что детерминированное освобождение памяти является довольно фундаментальной проблемой в структурах без блокировки. (от Блокированные структуры данных Автор: Андрей Александреску)

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

Просто, чтобы быть ясным здесь: это не означает, что ЦЕЛЫЙ МИР должен быть собран мусором, как на Java; только соответствующие данные должны быть аккуратно собраны мусором.


В одном из своих выступлений Bjarne Stroustrup также дал хороший, верный пример, где GC становится удобным. Представьте себе приложение, написанное на C/С++, 10M SLOC по размеру. Приложение работает достаточно хорошо (довольно безнадежно), но оно протекает. У вас нет ресурсов (человеко-часов) или функциональных знаний, чтобы исправить это. Исходный код - несколько грязный старый код. Чем ты занимаешься? Я согласен, что это, пожалуй, самый простой и самый дешевый способ поднять проблему под ковер с GC.


Как было указано sasha.sochka, сборщик мусора будет необязательным.

Моя личная забота заключается в том, что люди начнут использовать GC, как это используется на Java, и будут писать неряшливый код и мусор собирать все. (У меня сложилось впечатление, что shared_ptr уже стал по умолчанию "идти" даже в случаях, когда unique_ptr или, черт возьми, будет выполняться его распределение по стекам.)

Ответ 2

Я согласен с @DeadMG, что в текущем стандарте С++ нет GC, но я хотел бы добавить следующую цитату из B. Stroustrup:

Когда (если нет) автоматическая сборка мусора становится частью С++, это будет необязательным

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

Если он не изменит свое мнение, мы можем ожидать, что он будет добавлен и реализован в будущем.

Ответ 3

Есть некоторые алгоритмы, которые сложны/неэффективны/невозможны для записи без GC. Я подозреваю, что это основная точка продаж для GC на С++ и никогда не может увидеть, что он используется как распределитель общего назначения.

Почему не распределитель общего назначения?

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

Во-вторых, вам нужно будет установить некоторые очень не-С++-подобные ограничения на то, как вы можете использовать память. Например, вам понадобится хотя бы один доступный, un-obfuscated указатель. Obfuscated указатели, как популярны в общих библиотеках контейнеров дерева (с использованием низкоуровневых минимальных бит для цветовых флагов), среди прочих, не будут распознаваться GC.

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

Чтобы сделать объект безопасным перемещением, GC должен иметь возможность обновлять все указатели. Он не сможет найти обфускации. Это может быть размещено, но не будет довольно (вероятно, тип gc_pin или аналогичный, используемый как текущий std::lock_guard), который используется всякий раз, когда вам нужен необработанный указатель). Удобство использования было бы в стороне.

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

Причины использования (управление ресурсами) и соображения эффективности (быстрые, подвижные ассигнования) в сторону, что еще полезно для GC? Конечно, не общего назначения. Введите блокирующие алгоритмы.

Почему блокировка?

Алгоритмы блокировки работают, позволяя операции под конфликтом временно "не синхронизироваться" с структурой данных и обнаруживая/исправляя это на более позднем этапе. Один из последствий этого заключается в том, что в конфликтную память можно получить доступ после ее удаления. Например, если у вас есть несколько потоков, конкурирующих с pop node из LIFO, возможно, чтобы один поток выскочил и удалил node, прежде чем другой поток понял, что node уже был занят:

Тема А:

  • Получить указатель на root node.
  • Получить указатель на следующий node от root node.
  • Приостановить

Тема B:

  • Получить указатель на root node.
  • Приостановить

Тема А:

  • Поп node. (замените указатель root node следующим указателем node, если указатель root node не изменился с момента его чтения.)
  • Удалить node.
  • Приостановить

Тема B:

  • Получить указатель на следующий node из нашего указателя root node, который теперь "не синхронизирован" и был просто удален, поэтому вместо этого мы сбой.

С помощью GC вы можете избежать возможности чтения из незафиксированной памяти, потому что node никогда не будет удален, пока Thread B ссылается на него. Есть способы обойти это, например указатели опасности или перехватывать исключения SEH в Windows, но они могут значительно ухудшить производительность. GC является наиболее оптимальным решением здесь.

Ответ 4

Нет, потому что его нет. Единственные функции, которые С++ когда-либо имели для GC, были представлены на С++ 11, и они просто маркируют память, там не требуется коллекционер. Также не будет в С++ 14.

В ад не может пройти коллектор, который может пройти Комитет, это мое мнение.

Ответ 5

GC имеет следующие преимущества:

  • Он может обрабатывать циклические ссылки без помощи программистов (с RAII-стилем, вы должны использовать weak_ptr для разрыва кругов). Таким образом, приложение стиля RAII может "просачиваться", если оно используется неправильно.
  • Создание/уничтожение тонн shared_ptr для данного объекта может быть дорогостоящим, потому что приращение/уменьшение refcount являются атомарными. В многопоточных приложениях ячейки памяти, которые содержат refcounts, будут "горячими" местами, оказывая значительное давление на подсистему памяти. GC не подвержен этой конкретной проблеме, поскольку он использует доступные наборы вместо refcounts.

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

Ответ 6

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

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

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

Ответ 7

Определения:

RCB GC: GC, основанный на подсчете ссылок.

MSB GC: GC на основе Mark-Sweep.

Быстрый ответ:

MSB GC следует добавить в стандарт С++, потому что в некоторых случаях он более удобен, чем RCB GC.

Два иллюстративных примера:

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

Реализация 1 (версия MSB GC):

int*   g_buf = 0;
size_t g_current_buf_size = 1024;

void InitializeGlobalBuffer()
{
    g_buf = gcnew int[g_current_buf_size];
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = gcnew int[new_size];
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 
    }   
}

Реализация 2 (версия RCB GC):

std::shared_ptr<int> g_buf;
size_t g_current_buf_size = 1024;

std::shared_ptr<int> NewBuffer(size_t size)
{
    return std::shared_ptr<int>(new int[size], []( int *p ) { delete[] p; });
}

void InitializeGlobalBuffer()
{
    g_buf = NewBuffer(g_current_buf_size);
}

int GetValueFromGlobalBuffer(size_t index)
{
    return g_buf[index];
}

void EnlargeGlobalBufferSize(size_t new_size)
{
    if (new_size > g_current_buf_size)
    {
        auto tmp_buf = NewBuffer(new_size);
        memcpy(tmp_buf, g_buf, g_current_buf_size * sizeof(int));       
        std::swap(tmp_buf, g_buf); 

        //
        // Now tmp_buf owns the old g_buf, when tmp_buf is destructed,
        // the old g_buf will also be deleted. 
        //      
    }   
}

ОБРАТИТЕ ВНИМАНИЕ:

После вызова std::swap(tmp_buf, g_buf);, tmp_buf принадлежит старая g_buf. Когда tmp_buf уничтожается, старый g_buf также будет удален.

Если другой поток вызывает GetValueFromGlobalBuffer(index); для извлечения значения из старого g_buf, тогда будет происходить расовая опасность!!!

Итак, хотя реализация 2 выглядит так же элегантно, как и реализация 1, она не работает!

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

Вывод:

Хорошо взять MSB GC в стандарт С++ в качестве дополнительной функции.