Питання Навіщо збирати сміття, коли доступний RAII?


Я чую розмови про C ++ 14, що представляє смітник в самому стандартному бібліотеці C ++. Яке обґрунтування цієї функції? Чи не є це причиною існування RAII в C ++?

  • Як наявність стандартного збирача сміття збирається на RAII семантику?
  • Як це важливо для мене (програміста) чи способу написання програм C + +?

41
2018-06-23 14:48


походження


Я чув, коли Херб сказав, що це буде дуже корисним для деяких ситуацій без блокування програмування, але оскільки я не знаю про це, я не буду намагатися відповідати - Andy Prowl
Безпоміжна річ проста: багато безблоковних алгоритмів надзвичайно прості, якщо вам дозволяється витік пам'яті. - R. Martinho Fernandes
@ Р. Мартіньо Фернандес: увага до розробки / навести приклад? - Xeo
Крім того, говорить Херб ні ВСЕ повинні бути зібрані зі сміттям, лише речі, пов'язані з беззаботною справою. Він також заявляє, що GC має бути точним. - Ali
@ xmllmx shared_ptr недостатньо. Уявіть собі потік, який зчитує вміст shared_ptr і негайно попереджає потоком видалення вузла. Коли він відновлює виконання, він не може визначити, що вміст, який він читає, все ще є дійсним, оскільки він ніколи не збільшував refcount, і для видалення потоку цілком законно було звільнити вузол. Це складна проблема, але вона може бути вирішена EBR, RCU, покажчиками ризику, повнофункціональними GC або контекстно-чутливими рішеннями. - Ze Blob


Відповіді:


Сміттєві збірки та RAII корисні в різних контекстах. Наявність ГК не повинно впливати на використання RAII. Оскільки RAII добре відомий, я наведу два приклади, де GC зручний.


Сміттєві брандмаутери могли б допомогти у створенні структур даних без блокування.

[...] Виявляється, що детерміністичне звільнення пам'яті є досить фундаментальною проблемою в безконтактних структурах даних. (від Безконтактні структури даних Автор Андрій Александреску)

В основному проблема полягає в тому, що ви повинні переконатися, що ви не вилучаєте пам'ять під час читання нитки. Ось де GC стає зручним: він може дивитися на потоки і тільки робити вилучення, коли це безпечно. Будь ласка, прочитайте статтю для подробиць.

Просто ясно тут: це не означає, що ЦІЛИЙ СВІТ слід збирати сміття, як у Java; лише релевантні дані повинні точно збирати сміття.


У одному з його презентацій Бярне Струструп також дав хороший приклад, де GC стає зручним. Уявіть, що програма написана на C / C ++, 10M SLOC розміром. Програма працює досить добре (досить помилково), але вона витікає. Ви не маєте ресурсів (годинник), ані функціональних знань, щоб це виправити. Вихідний код - це трохи заплутаний кодекс спадщини. Що ти робиш? Я згоден з тим, що це, мабуть, найпростіший і найдешевший спосіб розібрати проблему під килим з GC.


Як вже зазначалося саша.сочка, the сміттєзвалище буде необов'язковим.

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


30
2018-06-24 08:43



Збирання сміття також може стати в нагоді, коли потрібно використовувати купу, але звільнення об'єктів відбувається дуже часто. Забір сміття дозволяє відкладати звільнення ресурсів до тих пір, поки пам'ять не буде достатньо, а потім зібрати всю пам'ять відразу. якщо програма не оптимізована для використання пулів пам'яті або безкоштовних списків, але страждає від цієї проблеми, це може допомогти цьому фактові. - Alex
@Alex: існують інші способи обробки такої речі, як, наприклад, спеціальні розподілювачі / розблокування та інтелектуальний покажчик - akappa
@akappa: чому об'єднувати накопичувач вручну вручну, якщо ви можете мати об'ємний смітник для сміття, який виконує те ж саме, що має еквівалентну продуктивність? - Alex
@Alex: я вважаю за краще мати бібліотечні, неінвазивні та явний рішення щось на кшталт "вставлення GC в runtime C ++", які звучать дуже інвазивно і тонко. Але це лише моя перевага, тому я можу зрозуміти, чому люди роблять різні речі. - akappa
@akappa: коли GC буде додано до C ++, ми сподіваємося, що все ще буде C ++. Значення: усе, що коштує результативності, буде доступним. (вам не потрібно його використовувати). І, маючи GC, коли продуктивність буде кращою, ніж розумні покажчики, теж приємно мати. Я створив інтелектуальний покажчик + басейн + спеціальний розблокування, щоб відкласти деалокацію та зменшити тиск нахилу, просто маючи це як функцію мови замість ручного коду, дуже приємно. - Alex


Я погоджуюсь з @DeadMG, що в поточному стандарті C ++ немає GC, але я хотів би додати наступну цитату з Б. Струструппа:

Коли (а не якщо) автоматична колекція сміття стане частиною C + +, це   буде необов'язковим

Тому Бьярна впевнена, що вона буде додана в майбутньому. Принаймні, голова EWG (робоча група Evolution) та один з найважливіших членів комітету (і, що більш важливо, творець мови) хоче додати його.

Якщо він не змінив свою думку, ми можемо сподіватися, що його буде додано та впроваджено в майбутньому.


11
2018-06-23 15:53



Насправді, "головою" (або, точніше, організатором) комітету С ++ є Герб Саттер. Bjarne Stroustrup очолює Робочу групу Evolution (EWG). - Cassio Neri
@ sasha.sochka: Що ви думаєте про мою відповідь? Я не впевнений, що він погоджується з тим, що насправді люди думають про EWG, але це, здавалося б, дуже важливою перевагою того, як мова розпізнає GC, а не намагається впровадити GC через бібліотеку, про яку мова не знає. - supercat
@supercat, я думаю, ваша відповідь є розумною. Я нічого не знаю про плани робочої групи, але той факт, що Струструп обіцяє зробити факультативний варіант GC, робить дуже реалістичним реалізацію того, щоб GC було реалізовано на самому мові. Старий код не використовуватиме GC, а новий код вибере режим, наприклад, використовуючи певний прапор компілятора. +1 - sasha.sochka


Є кілька алгоритмів, які є складними / неефективними / неможливими для запису без GC. Я підозрюю, що це основний пункт продажу для GC в C ++, і він ніколи не бачить, що він використовується як розподільник загального призначення.

Чому б не розподільник загального призначення?

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

По-друге, вам доведеться розмістити деякі дуже не-C + + -подібні обмеження на те, як можна використовувати пам'ять. Наприклад, вам знадобиться, принаймні, один доступний, неприхований покажчик. Захищені покажчики, як і популярні в загальних бібліотеках контейнерів дерев (з використанням гарантованих вирівнюванням низьких бітів для кольорових прапорів) серед іншого, не будуть впізнавані GC.

У зв'язку з цим речі, які роблять сучасні GC настільки корисними, будуть дуже важко застосовуватись до C ++, якщо ви підтримуєте будь-який кількість заплутаних покажчиків. Генераційні дефрагментації GC дійсно класні, оскільки виділення є надзвичайно дешевим (суттєво просто збільшуючи покажчик), і в кінцевому рахунку ваші асигнування усуваються на щось менше, з покращеною місцевістю. Для цього об'єкти повинні бути рухомими.

Щоб об'єкт безпечно рухливий, GC повинен мати можливість оновлювати всі покажчики на нього. Вона не зможе знайти захоплених. Це може бути розміщене, але не було б гарним (можливо, а gc_pin тип або подібний, використовується як поточний std::lock_guard, який використовується всякий раз, коли вам потрібен сирий покажчик). Юзабілітет буде поза дверями.

Без створення рухомих елементів GC буде значно повільніше і менш масштабований, ніж той, який ви використовуєте в інших місцях.

Причини використання (керування ресурсами) та причини ефективності (швидкі, рухомі розподіли) не збігаються, а що ж для ГК? Звичайно, не загальний спосіб. Введіть без блокування алгоритми.

Чому замок без блокування?

Алгоритми без блокування працюють, дозволяючи операції під час суперечки йти тимчасово "не синхронізуватися" з структурою даних і виявляти / виправляти це на більш пізньому етапі. Одним з ефектів цього є те, що під контурністю пам'ять може бути доступна після її видалення. Наприклад, якщо у вас є декілька ниток, що конкурують з поп-вузлом з LIFO, для одного потоку можна запустити і видалити вузол, перш ніж інший потік зрозумів, що вузол вже був запущений:

Тема A:

  • Отримати покажчик на кореневий вузол.
  • Отримати покажчик на наступний вузол з кореневого вузла.
  • Призупинити

Тема B:

  • Отримати покажчик на кореневий вузол.
  • Призупинити

Тема A:

  • Поп-вузол. (замініть курсор рухомого вузла на наступний вузловий покажчик, якщо покажчик корневого вузла не змінювався з моменту його читання.)
  • Видалити вузол.
  • Призупинити

Тема B:

  • Отримати покажчик на наступний вузол від нашого покажчика кореневого вузла, який зараз "не синхронізовано" і був просто видалений, а замість цього ми збоїмося.

За допомогою GC ви можете уникнути можливості читання з невстановленої пам'яті, тому що вузол ніколи не буде видалено, тоді як Thread B посилання на нього. Є способи навколо цього, наприклад покажчики небезпеки або вилучення SEH-винятків у Windows, але це може значно погіршити продуктивність. GC, як правило, є найбільш оптимальним рішенням тут.


11
2018-06-23 17:20



Де це пояснення стосується, зокрема, алгоритмів без блокування? Це просто виглядає як основний стан перегонів, який можна вирішити без обов'язкового вживання GC. Ви скажете з GC, що це не спричинить аварію, тому що вузол ніколи не буде видалено, тоді як Thread B вкаже на неї (в кроці 2 посилання B обов'язково одержує кореневий вузол), так що буде добре, читати, що було спливав, але не видалено (крок 4)? - pepper_chico
Так, без замків LIFO може прочитати спливаючий вузол. Це очікувана поведінка. З GC не буде delete, тому неможливо отримати доступ до можливої ​​невстановленої пам'яті. - Cory Nelson
Я вірю, що зрозумів, але не досить добре пояснив. Вибачте Безблокований LIFO pop() йде так: читай заголовок ptr від структури даних; читати наступний ptr від голови ptr (який на даний момент може бути не справжнім головним вузлом); атомно замінити головою ptr в структурі даних з наступним ptr, якщо голова ptr не зміниться. Під час цього немає замків, тому під час суперечки Thread A можна завершити повним pop() прямо посередині нитки B pop(). Очікується, що поток буде читати застарілі дані під суперечкою, а останній крок, який атомний виправляє для цього. - Cory Nelson
Такі структури даних можуть працювати краще з GC, ніж RAII, але вони здаються неясними. Рядки дуже поширені, і вони також можуть працювати краще з GC, ніж RAII (хоча вони можуть працювати ще краще на мові, яка підтримує обидва). Кожного разу, коли посилання на стійку послідовності створюється або руйнується в системі, що не є системою GC, загалом необхідно використовувати атомні операції, щоб з'ясувати, чи це посилання є останнім. Атомні операції дорогі на багатопроцесорних системах. У системі GC, збереження посилання в рядку вимагає всього лише копіювання посилання. - supercat
Хоча можна уникнути ускладнень збереження посилання на рядки, маючи копію коду вміст будь-якого рядка, який він хоче зберегти, що може додати значну вартість часу виконання. Це не тільки збільшує споживання пам'яті та збільшує час, витрачений на копіювання рядків - це також перешкоджає швидкому виявленню рядків, які є рівноцінними. RAII-підтримка може бути використана для розрізнення рядків, які не можуть мати більше одного посилання з типів, які "могло б", що дозволило б модифікувати рядки на місці, для яких може існувати лише одна посилання, але GC є корисним для рядків. - supercat


Не існує, тому що не існує. Єдині функції C ++, які коли-небудь були для GC, були введені в C ++ 11, і вони просто маркування пам'яті, колектора не потрібно. Також не буде в C ++ 14.

У пеклі немає шляху, коли колекціонер міг би пройти комітет, це моя думка.


6
2018-06-23 15:01



+1; також проект комітету C ++ 14 не вводить сміттєві збірки: isocpp.org/files/papers/N3690.pdf - Nate Kohl
Це не відповідає на питання. Ви навіть не пояснюєте, чому у вас така сильна думка. - anatolyg
@anatolyg: Питання полягає в тому, які наслідки збору сміття в C ++ 14. Ніхто не існує, бо немає нікого. Це відповідь на його питання. Моя думка - це лише примітка, дійсно. - Puppy


Жоден з відповідей поки не стосується найважливішої переваги додавання сміття-сміття до мови: за відсутності підтримуваної мовою сміттєвої колекції, майже неможливо гарантувати, що жоден об'єкт не буде знищений, поки посилання на нього існують. Гірше того, якщо така річ станеться, майже неможливо гарантувати, що пізніша спроба використовувати посилання не призведе до маніпулювання іншим випадковим об'єктом.

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

Незважаючи на те, що існують способи здійснення збирачів сміття без належної мовної підтримки, такі реалізації вимагають, щоб GC було інформовано про те, що будь-які часові посилання створюються або руйнуються (додавши значні клопоти і накладні витрати) або ризикують, що довідник, який GC не знає про існування може існувати до об'єкта, який інакше не пов'язаний. Підтримка компілятора для GC усуває обидва ці проблеми.


4
2017-07-23 20:27



Якщо щось намагається використовувати посилання на руйнується об'єкт, то у вашій програмі є помилка. Використання GC для приховання проблеми не допоможе вам у довгостроковій перспективі. - Andrew Durward
@AndrewDurward: у випадках, коли об'єкти GC можуть приймати повідомлення про те, що вони більше не потрібні, вони можуть бути закодовані явна ловушка на спроби їх використовувати після цього. GC не приховує проблему - навпаки. Замість того, щоб програма поводилася випадково, вона буде детерміновано лову по помилці. Крім того, багато сценаріїв, які можуть призвести до руйнування помилок в C ++, будуть невирішеними в .NET та Java. Наприклад, в рамках GC можна спокійно передавати посилання на незмінні структури даних між потоками, а нитки можуть використовувати ці посилання, як вони вважають потрібним ... - supercat
... без необхідності будь-якої синхронізації пам'яті. У C ++, якщо декілька потоків можуть створювати та знищувати посилання на незмінний об'єкт, тоді як якщо кожен код, який може збільшувати або зменшувати кількість існуючих посилань, використовує синхронізацію пам'яті при оновленні будь-якого еталонного підрахунку, то важко буде уникнути таких висновків синхронізації - supercat


GC має такі переваги:

  1. Він може обробляти круглі посилання без допомоги програміста (у стилі RAII, вам слід використовувати weak_ptr, щоб розбити кола). Таким чином, додаток RAII стилю може все ще "витікати", якщо він використовується неналежним чином.
  2. Створення / знищення тонн shared_ptr для даного об'єкта може бути дорогим, оскільки приріст / зменшення refcount - це атомні операції. У багатопотокових додатках місця пам'яті, які містять реф-рахунки, стануть "гарячими" місцями, створюючи великий тиск на підсистему пам'яті. GC не схильний до цієї конкретної проблеми, оскільки використовує доступні набори, а не refcounts.

Я не кажу, що GC є найкращим / гарним вибором. Я просто кажу, що він має різні характеристики. У деяких сценаріях це може бути перевагою.


4
2018-06-24 10:38



Зауважте, що навіть за допомогою GC ви можете "протікати". Уявіть собі обробника подій, де ви реєструєте делегатів, але забудьте скасувати реєстрацію цих делегатів, коли ви закінчите з об'єктом. З моменту, коли ви забули зареєструвати, ви витікте об'єкт, оскільки він ніколи не буде зібраний GC, оскільки є ще дійсне посилання на нього. Тепер уявіть, що делегат тримає посилання на якийсь великий об'єкт -> удар! - MFH
@MFH Так, все ще можна мати логічні витоки, хоча це набагато рідше. Це не викликає єдині довідкові цикли. У вашому прикладі, витік виникає лише тоді, коли делегат є сильно доступним (імовірно, через список обробників подій). Це може статися, якщо подія глобальна / статична, але це не так у багатьох випадках. Також зауважте, що це так ні витоку в тому сенсі, що більше неможливо отримати доступ до неї: оскільки подія може бути доступною, її можна буде викликати пізніше. Якщо це так, у вас все одно є помилка. Лише у тому випадку, якщо ви не маєте витоку, але це ще рідше.
Програма RAII не матиме "тонн shared_ptr's" - Cubbi
Набагато більша перевага компіляції, що підтримується збиранням сміття, полягає в тому, що, поки існує посилання на мертвий об'єкт, гарантовано завжди звертатися до того самого мертвого об'єкта. На відміну від цього, при використанні RAII, посилання (або покажчик) на мертвий об'єкт може спонтанно перетворитися на явно дійсне посилання на якийсь довільний живий об'єкт. Витоки пам'яті погані, але болісні посилання гірше; RAII не може дуже добре запобігти зависненню посилань на заклик до Undefined Behavior, однак збирач сміття може підтримувати компілятор. - supercat


Визначення:

RCB GC: Reference Counter Based GC.

MSB GC: GC, що базується на маркіровці.

Швидкий відповідь: 

MSB GC слід додати до стандарту C ++, оскільки в деяких випадках це більш зручно, ніж 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 у стандарт C ++ як необов'язкову функцію.


1
2017-09-15 06:47