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

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

  • Як наявність стандартного збирача сміття з бібліотеки вплине на семантику RAII?
  • Як це важливо для мене (програміста) чи способу написання програм C + +?
41
@ Ze, може std :: shared_ptr не вирішити задану проблему елегантно? наприклад, використовуйте std :: shared_ptr <�вузол> замість вузла *, як наступний тип поля.
додано Автор xmllmx, джерело
Крім того, Герб каже не ВСЕ повинні бути зібрані сміття, тільки речі, пов'язані з без замків. Він також заявляє, що GC має бути точним.
додано Автор Ali, джерело
Безпоміжна річ проста: багато безблоковних алгоритмів надзвичайно прості, якщо вам дозволяється витік пам'яті.
додано Автор R. Martinho Fernandes, джерело
Я чув, коли Херб сказав, що це буде дуже корисним для деяких ситуацій без блокування програмування, але оскільки я не знаю про це, я не буду намагатися відповідати
додано Автор Andy Prowl, джерело
Де ти чув про C ++ 14, що отримує GC? Тому що це, звичайно, не в поточному робочому проекті.
додано Автор Nicol Bolas, джерело
@AndyProwl: теж саме тут. Сподіваємось, хтось, хто знає краще, спробує відповісти.
додано Автор Alok Save, джерело
@ Р. Мартіньо Фернандес: увага до розробки/навести приклад?
додано Автор Xeo, джерело
C ++ не є птахом в клітці, як object-C або .NET, вона використовується скрізь від мікроконтролерів до серверів, непередбачувані утиліти можуть викликати нові проблеми та повний контроль над пам'яттю, що робить C ++ більш переважним, ніж керовані мови.
додано Автор fatihk, джерело
Стандарт AFAIK лише побоюється достатньої кількості GC, щоб зробити можливим реалізацію, тому його можна використовувати як додатковий спосіб керування деякою кількістю пам'яті. GC не збирається замінювати RAII або об'єкт об'єкта обмеженого об'єкта, на якому він заснований.
додано Автор josefx, джерело
@ xmllmx shared_ptr недостатньо. Уявіть собі потік, який зчитує вміст shared_ptr і негайно попереджає потоком видалення вузла. Коли він відновлює виконання, він не може визначити, що вміст, який він читає, все ще є дійсним, оскільки він ніколи не збільшував refcount, і для видалення потоку цілком законно було звільнити вузол. Це складна проблема, але вона може бути вирішена EBR, RCU, покажчиками ризику, повнофункціональними GC або контекстно-чутливими рішеннями.
додано Автор Ze Blob, джерело
@Xeo Зніміть елемент із приєднаного списку. Видалення його дуже просто, простий кас, і ти закінчиш ... Ну не зовсім; коли безпечно видалити вузол? В даний час будь-яка кількість інших потоків може мати посилання на цей вузол, і дуже важко визначити, коли це безпечно видалити. GC легко піклується про цю проблему, маючи інший потік, виконує весь підрахунок підрахунків для вас у безпечному режимі. Особисто я сильніше віддаю перевагу RCU , яка набагато менш інвазивна і мала велику справу успіху в ядрі Linux.
додано Автор Ze Blob, джерело

7 Відповіді

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


Збір сміття буде відмінною справою в організації структур даних без блокування.

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

Андрія Александраску)

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

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


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


Як зазначалося sasha.sochka , колектор сміття буде необов'язковим .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Визначення:

RCB GC: Reference Counter Based GC.

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

Quick Answer:

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 g_buf;
size_t g_current_buf_size = 1024;

std::shared_ptr NewBuffer(size_t size)
{
    return std::shared_ptr(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 також буде видалено.

If another thread is calling GetValueFromGlobalBuffer(index); to fetch the value from the old g_buf, then A Race Hazard Will Occur!!!

Отже, хоча реалізація 2 виглядає так же елегантно, як і реалізація 1, вона не працює!

Якщо ми хочемо правильно виконати реалізацію 2, ми повинні додати певний замок-механізм; то це буде не тільки повільніше, але менш елегантно, ніж реалізація 1.

Висновок:

Добре прийняти MSB GC у стандарт C ++ як необов'язкову функцію.

1
додано
IT KPI C/С++ новым годом
IT KPI C/С++ новым годом
747 учасників

Чат обсуждения С/С++. - Вопросы "напишите за меня лабу" - это оффтоп. - Оффтоп, флуд, оскорбления и вбросы здесь не приняты. - За нарушение - предупреждение или mute на неделю. - За спам и рекламу - ban. Все чаты IT KPI: https://t.me/itkpi/1147