Чому C ++ не має сміттєзбірника?


270

Я не задаю цього питання через переваги перевезення сміття насамперед. Моя основна причина запитати це те, що я знаю, що Б'ярн Струструп сказав, що C ++ в якийсь момент матиме сміттєзбірник.

З урахуванням сказаного, чому це не було додано? Вже є кілька сміттєзбірників для C ++. Це лише одна з речей типу "легше сказати, ніж зробити"? Або є інші причини, коли він не був доданий (і не буде доданий у C ++ 11)?

Поперечні посилання:

Для уточнення я розумію причини, чому C ++ не мав сміттєзбірника під час його створення. Мені цікаво, чому колектор не може бути доданий.


26
Це один із перших десяти міфів про C ++, які ненависні завжди викликають. Збір сміття не "вбудований", але є кілька простих способів зробити це C ++. Опублікував коментар, тому що інші вже відповіли краще, ніж я міг нижче :)
дав

5
Але в цьому вся суть про те, щоб не вбудовуватися, ви повинні зробити це самостійно. Реалізація від високої до низької: вбудована, бібліотечна, домашня. Я сам використовую C ++, і, безумовно, не ненавиджу, бо це найкраща мова у світі. Але динамічне управління пам’яттю - це біль.
QBziZ

4
@Davr - Я не ненависник C ++ і навіть не намагаюся навіть стверджувати, що C ++ потребує сміттєзбірника. Я запитую, бо знаю, що Б'ярн Струструп сказав, що це буде додано, і було просто цікаво, в чому полягають причини його невтілення.
Джейсон Бейкер

1
У цій статті Колекціонер Бома для C та C ++ від доктора Доббса описує сміттєзбірник із відкритим кодом, який можна використовувати як з C, так і з C+. У ньому обговорюються деякі проблеми, які виникають із використанням сміттєзбірника з деструкторами C ++, а також зі стандартною бібліотекою C.
Річард Чемберс

1
@rogerdpack: Але це вже не так корисно (див. мою відповідь ...), тому навряд чи реалізація буде вкладати кошти в її створення.
einpoklum

Відповіді:


160

Непомітне збирання сміття могло бути додано, але це просто не вирізало. Можливо, через не лише ускладнення щодо впровадження, але й через те, що люди не змогли досягти загального консенсусу досить швидко.

Цитата самого Б'ярне Струструпа:

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

Існує гарне обговорення цієї теми тут .

Загальний огляд:

C ++ дуже потужний і дозволяє робити майже все. З цієї причини він не автоматично натискає на вас багато речей, що може вплинути на ефективність роботи. Збір сміття може бути легко реалізований за допомогою розумних покажчиків (об’єктів, які обертають покажчики з опорним підрахунком, які автоматично видаляють себе, коли кількість посилань досягає 0).

C ++ був побудований з урахуванням конкурентів, які не мали сміття. Основна стурбованість, яку C ++ довелося відбивати від критики, порівняно з C та іншими.

Існує 2 види збору сміття ...

Явний збір сміття:

C ++ 0x матиме збір сміття за допомогою покажчиків, створених з shared_ptr

Якщо ви хочете, ви можете використовувати його, якщо ви цього не хочете, ви не змушені його використовувати.

На даний момент ви можете також використовувати boost: shared_ptr, якщо ви не хочете чекати C ++ 0x.

Неявне збирання сміття:

Однак він не має прозорого збору сміття. Це буде фокусним пунктом для майбутніх специфікацій C ++.

Чому у Tr1 немає неявного збирання сміття?

Є багато речей, які повинен був мати tr1 з C ++ 0x, Б'ярн Струструп у попередніх інтерв'ю заявив, що у tr1 не було стільки, скільки він хотів би.


71
Я став би ненависником, якби C ++ примусово збирав сміття на мене! Чому люди не можуть використовувати smart_ptr's? Як би ви зробили розщеплення в стилі Unix, зі збирачем сміття? Будуть зачеплені інші речі, такі як нарізка різьби. У Python є глобальний перекладач інтерпретації, головним чином через його збирання сміття (див. Cython). Дякую, щоб не було C / C ++, дякую.
unixman83

26
@ unixman83: Основна проблема з посиланням на сміття (тобто std::shared_ptr) - це циклічні посилання, які спричиняють витік пам'яті. Тому ви повинні обережно використовувати std::weak_ptrдля перерви циклів, що безладно. У стилі маркування та розгортання GC ця проблема не має. Не існує притаманної несумісності між нарізкою / вилканням та збиранням сміття. І Java, і C # мають високоефективну попереджувальну багатопоточність та збирач сміття. Існують проблеми, пов’язані з програмами в реальному часі та сміттєзбірниками, оскільки більшості сміттєзбірників доводиться зупиняти світ.
Андрій Томазос

9
"Основна проблема з посиланням на перерахований сміття (тобто std::shared_ptr) - це циклічні посилання" та жахливе виконання, яке є іронічним, тому що краща продуктивність зазвичай є виправданням для використання C ++ ... flyingfrogblog.blogspot.co.uk/2011/01/…
Джон Харроп

11
"Як би ви зробили форкінг для стилю Unix низького рівня". Так само, як мови GC'd, такі як OCaml, роблять це протягом ~ 20 років і більше.
Джон Харроп

9
"У Python є глобальний перекладач інтерпретації, головним чином через його збирання сміття". Аргумент Соломена. І Java, і .NET мають GC, але не мають глобальних блокувань.
Джон Харроп

149

Щоб додати до дебатів тут.

Існують відомі проблеми зі збиранням сміття, і їх розуміння допомагає зрозуміти, чому їх немає в C ++.

1. Продуктивність?

Перша скарга часто стосується продуктивності, але більшість людей насправді не розуміють, про що вони говорять. Як проілюстровано Martin Beckettпроблемою, може бути не ефективність як такої, а передбачуваність виконання.

В даний час є 2 родини GC, які широко розгорнуті:

  • Марк-і-підмітаючий вид
  • Довідково-лічильний вид

Більш Mark And Sweepшвидкий (менший вплив на загальну продуктивність), але він страждає від синдрому "заморозити світ": тобто коли GC запускає, все інше зупиняється, поки GC не очистить. Якщо ви хочете створити сервер, який відповідає за кілька мілісекунд ... деякі транзакції не виправдають ваших очікувань :)

Проблема Reference Countingполягає в іншому: підрахунок посилань додає накладні витрати, особливо в середовищі Multi-Threading, оскільки вам потрібно мати кількість атомів. Крім того, існує проблема еталонних циклів, тому вам потрібен розумний алгоритм, щоб виявити ці цикли та усунути їх (як правило, впроваджуйте за допомогою "заморожування світу", хоча і рідше). Взагалі, на сьогоднішній день цей вид (навіть як правило, більш чуйний або, швидше, замерзає рідше) повільніше, ніж Mark And Sweep.

Я бачив статтю виконавців Ейфеля, які намагалися реалізувати Reference CountingКолекціонер сміття, який мав би схожу глобальну ефективність Mark And Sweepбез аспекту "Freeze The World". Це вимагало окремої нитки для GC (типово). Алгоритм був дещо лякаючим (наприкінці), але в роботі було зроблено добру роботу, вводячи поняття по черзі та показуючи еволюцію алгоритму від "простої" версії до повноцінної. Рекомендовано прочитати, якби тільки я міг повернути руки на файл PDF ...

2. Придбання ресурсів - це ініціалізація (RAII)

Це загальна ідіома в Росії C++ тому, що ви загорнете право власності на ресурси в об'єкті, щоб забезпечити їх належне звільнення. В основному використовується для пам’яті, оскільки у нас немає сміття, але він також корисний для багатьох інших ситуацій:

  • замки (багатопотокова, файлова ручка, ...)
  • підключення (до бази даних, іншого сервера, ...)

Ідея полягає в тому, щоб правильно контролювати термін експлуатації об'єкта:

  • воно повинно бути живим, поки вам це потрібно
  • його потрібно вбити, коли ви закінчите з цим

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

Мови з GC мають два напрямки роботи:

  • не використовуйте GC, коли розподілу стека достатньо: це зазвичай для проблем із продуктивністю, але в нашому випадку це дійсно допомагає, оскільки область визначає термін експлуатації
  • usingконструювати ... але це явний (слабкий) RAII, тоді як у C ++ RAII неявний, так що користувач НЕ МОЖЕТЕ мимоволі зробити помилку (опустивши usingключове слово)

3. Розумні покажчики

Розумні вказівники часто виступають у вигляді срібної кулі для обробки пам’яті C++. Часто я чув, що GC нам не потрібен, оскільки ми маємо розумні покажчики.

Не можна було більше помилятися.

Розумні покажчики допомагають: auto_ptrі unique_ptrвикористовувати поняття RAII, справді надзвичайно корисні. Вони настільки прості, що написати їх самостійно можна досить легко.

Якщо вам потрібно поділитися правом власності, однак це стає складніше: ви можете поділитися між декількома потоками, і є кілька тонких проблем з обробкою рахунку. Тому, природно, йде назустрічshared_ptr .

Це чудово, саме для цього Boost, але це не срібна куля. Насправді, головна проблема з shared_ptrтим, що він імітує GC, реалізований, Reference Countingале вам потрібно реалізувати виявлення циклу самостійно ... Ург

Звичайно, це є weak_ptrсправді, але я, на жаль, вже побачив витоки пам'яті, незважаючи на використання shared_ptrцих циклів ... і коли ви перебуваєте в середовищі з декількома нитками, це надзвичайно важко виявити!

4. Яке рішення?

Срібної кулі немає, але, як завжди, це безумовно можливо. За відсутності GC потрібно чітко визначити право власності:

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

Так справді, було б чудово мати ГК ... однак це не тривіальне питання. А поки що нам просто треба засунути рукави.


2
Я хотів би прийняти дві відповіді! Це просто чудово. Варто зазначити одне, що стосується продуктивності, GC, який працює в окремому потоці, насправді є досить поширеним (він використовується в Java та .Net). Зрозуміло, це може бути неприйнятним у вбудованих системах.
Джейсон Бейкер

14
Лише два типи? Як "копіювати колекціонерів"? Покоління колекціонерів? Асоційовані одночасно колектори (включаючи важку бігову доріжку Бейкера в режимі реального часу)? Різні гібридні колектори? Людина, ясна незнання в галузі цієї галузі часом мене дивує.
ПРОСТО МОЕ правильне ДУМКУ

12
Я сказав, що було лише 2 типи? Я сказав, що було 2, які широко розгорнулися. Наскільки я знаю Python, Java та C # усі зараз використовують алгоритми Mark and Sweep (у Java раніше був алгоритм підрахунку посилань). Якщо бути точнішим, мені здається, що C # використовує Generational GC для незначних циклів, Mark and Sweep для основних циклів та Copying для боротьби з фрагментацією пам'яті; хоча я б заперечував, що серце алгоритму - це Марк І Підмітання. Чи знаєте ви якусь основну мову, яка використовує іншу технологію? Я завжди радий вчитися.
Маттьє М.

3
Ви щойно назвали одну основну мову, яка використовувала три.
ПРОСТО МОЕ правильне ДУМКА

3
Основна відмінність полягає в тому, що генераційному та інкрементальному GC не потрібно зупиняти світ на роботі, і ви можете змусити їх працювати в однопотокових системах без надмірних накладних витрат, періодично виконуючи ітерації обходу дерева під час доступу до покажчиків GC (фактор можна визначити кількість нових вузлів разом з основним передбаченням необхідності збирання). Ви можете взяти GC ще більше, включивши дані про те, де в коді відбулося створення / модифікація вузла, що може дозволити вам покращити ваші прогнози, і ви отримаєте Аналіз втечі безкоштовно з ним.
Keldon Alleyne

56

Який тип? чи слід оптимізувати вбудовані контролери пральної машини, стільникові телефони, робочі станції чи суперкомп'ютери?
Чи слід надавати пріоритет чуйності та завантаженню сервера?
він повинен використовувати багато пам'яті або багато процесора?

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

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


6
Я, безумовно, бачу вашу думку, але я змушений запитати: чи не використовується Java майже у багатьох програмах?
Джейсон Бейкер

35
Ні. Java не підходить для високопродуктивних додатків з тієї простої причини, що у неї немає гарантій продуктивності в тій же мірі, що і для C ++. Таким чином, ви знайдете його в мобільному телефоні, але ви не знайдете його в мобільному комутаторі або суперкомп'ютері.
Затрус

11
Ви завжди можете придбати більше сервера, але не завжди можете придбати більше процесора для мобільного телефону, який вже є в кишені клієнта!
Crashworks

8
Java зробила велику ефективність роботи процесора. Справді нерозв'язна проблема - це використання пам'яті, Java за своєю суттю менш ефективна пам'ять, ніж C ++. А ця неефективність пов’язана з тим, що воно збирається сміттям. Збір сміття не може бути швидким та ефективним для пам'яті, що стає очевидним, якщо вивчити, наскільки швидко працюють алгоритми GC.
Nate CK

2
@Zathrus java може виграти на пропускній здатності оптимізуючого jit, хоча не затримки (boo в режимі реального часу) і, звичайно, не слід пам'яті.
gtrak

34

Однією з найважливіших причин того, що C ++ не має вбудованого сміття, є те, що отримати сміттєзбірник, щоб грати добре з деструкторами, дуже важко. Наскільки мені відомо, ніхто ще не знає, як це повністю вирішити. Є багато питань, з якими можна вирішити:

  • детерміновані терміни експлуатації об’єктів (підрахунок посилань дає вам це, але GC - ні. Хоча це може бути не так вже й багато).
  • що станеться, якщо деструктор кидає, коли об’єкт збирає сміття? Більшість мов ігнорує цей виняток, оскільки у них справді немає блоку лову, щоб можна було транспортувати його, але це, мабуть, не прийнятне рішення для C ++.
  • Як увімкнути / вимкнути це? Природно, це, мабуть, буде рішення про час компіляції, але код, який записаний для GC проти коду, який написаний для NOT GC, буде дуже різним і, ймовірно, несумісним. Як ви це погоджуєте?

Це лише деякі проблеми, з якими стикаються.


17
GC та деструктори - це вирішена проблема, приємним кроком від Bjarne. Деструктори не працюють під час GC, оскільки це не суть GC. GC в C ++ існує для створення поняття нескінченної пам'яті , а не нескінченних інших ресурсів.
MSalters

2
Якщо деструктори не запускаються, це повністю змінює семантику мови. Думаю, щонайменше, вам знадобиться нове ключове слово "gcnew" або щось таке, щоб ви явно дозволяли цьому об'єкту бути GC'ed (і тому ви не повинні використовувати його для загортання ресурсів, крім пам'яті).
Грег Роджерс

7
Це хибний аргумент. Оскільки C ++ має явне управління пам’яттю, вам потрібно з’ясувати, коли кожен об’єкт повинен бути звільнений. З ГХ це не гірше; швидше, проблема зводиться до з'ясування, коли звільняються певні об'єкти, а саме ті об'єкти, які потребують особливих міркувань при видаленні. Досвід програмування на Java та C # показує, що переважна більшість об'єктів не вимагає особливих міркувань і їх можна безпечно залишити в GC. Як виявляється, одна з головних функцій деструкторів в C ++ - це звільнення дочірніх об'єктів, з якими GC обробляє вас автоматично.
Nate CK

2
@ NateC-K: Одне, що вдосконалюється в GC проти non-GC (можливо, найбільше) - це здатність міцної системи GC гарантувати, що кожна посилання продовжуватиме вказувати на один і той же об'єкт, поки існує посилання. Виклик Disposeоб’єкта може зробити його непридатним, але посилання, які вказували на об'єкт, коли він був живий, продовжуватимуть це робити і після того, як він мертвий. Навпаки, в системах, що не належать до GC, об’єкти можна видалити, поки існують посилання, і рідко існує будь-яке обмеження хаосу, який може бути зірваний, якщо використовується одна з цих посилань.
supercat

22

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

Зокрема, стандарт C ++ досить обережно визначає мову з точки зору зовнішньої спостережуваної поведінки, а не як реалізація досягає такої поведінки. У разі збору сміття, однак, не є практично не зовні спостерігається поведінка.

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

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

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

Незважаючи на це, він сильніший за те, що було запропоновано для C ++. Попередня пропозиція [попередження: PDF] (що були знижені) не гарантія взагалі нічого. На 28 сторінках пропозицій те, що ви потрапили у спосіб зовнішньої поведінки, - це одна (ненормативна) примітка:

[Примітка. Для програм, зібраних зі сміттям, високоякісна реалізована програма повинна намагатися максимально збільшити кількість недосяжної пам'яті, яку вона відновлює. —Закінчити примітку]

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

Навіть у кращому випадку ми отримуємо програми, які на основі тестування на Java , ймовірно, потребують приблизно в шість разів більше пам’яті, щоб працювати з тією ж швидкістю, що і зараз. Гірше, що збирання сміття було частиною Java з самого початку - C ++ встановлює достатньо обмежень щодо збору сміття, що майже напевно матиме ще гірше співвідношення витрат / вигод (навіть якщо ми виходитимемо за рамки того, що пропозиція гарантувала, і припустимо, що це буде певна вигода).

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


Я б стверджував, що навіть якщо хтось вказує, що для належної роботи всі об’єкти повинні бути видалені, і лише об’єкти, які були видалені, були б придатні до збору, підтримка компілятора для збору сміття відстеження посилань все ще може бути корисною, оскільки така мова може забезпечити що використання видаленого вказівника (посилання) було б гарантовано в пастці, а не спричиняє не визначену поведінку.
supercat

2
Навіть на Java, GC насправді не вказано робити щось корисне AFAIK. Це може закликати freeвас (де я маю на увазі freeаналогічну мові С). Але Java ніколи не гарантує виклик фіналізаторів чи щось подібне. Насправді, C ++ робить набагато більше, ніж Java для обігу записів у базу даних фіксованих файлів, промивання ручок файлів тощо. Java стверджує, що має "GC", але розробники Java повинні close()весь час ретельно телефонувати, і вони повинні бути дуже обізнані в управлінні ресурсами, обережно не зателефонувавши close()занадто рано чи занадто пізно. C ++ звільняє нас від цього. ... (продовження)
Аарон Макдейд

2
.. мій коментар мить тому не призначений для критики Java. Я просто зауважую, що термін "вивезення сміття" - це дуже дивний термін - це означає набагато менше, ніж люди думають, що це робить, і тому важко обговорити це, не зрозумівши, що це означає.
Аарон Макдейд

@AaronMcDaid Це правда, що GC взагалі не допомагає з ресурсами, що не належать до пам'яті. На щастя, такі ресурси виділяються досить рідко в порівнянні з пам'яттю. Більше того, понад 90% з них можна звільнити методом, який їх виділив, тому try (Whatever w=...) {...}вирішує це (і ви отримуєте попередження, коли забудете). Решта проблемні і для RAII. Викликати close()"весь час" означає, можливо, один раз на десятки тисяч рядків, тому це не так вже й погано, тоді як пам'ять виділяється майже на кожній лінії Java.
maaartinus

15

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

Крім того, C ++ підтримує методи програмування, які дозволяють керувати пам'яттю безпечно та неявно без збирача сміття . Я вважаю збір сміття останнім вибором і недосконалим способом поводження з ресурсами. Це не означає, що це ніколи не корисно, просто, що в багатьох ситуаціях є кращі підходи.

Джерело: http://www.stroustrup.com/bs_faq.html#garbage-collection

Що стосується того, чому він не має його вбудований, якщо я добре пам'ятаю, він був винайдений до того, як GC була річчю , і я не вірю, що мова могла мати GC з кількох причин (IE Backwards сумісність з C)

Сподіваюсь, це допомагає.


"із виконанням, яке вигідно порівнює інші мови, зібрані зі сміттям". Цитування?
Джон Харроп

1
Моє посилання було розірвано. Цю відповідь я написав 5 років тому.
Рейн

1
Гаразд, я сподівався на якусь незалежну перевірку цих претензій, тобто не на Stroustrup чи Boehm. :-)
Джон Харроп

12

Stroustrup зробив кілька хороших коментарів до цього на конференції Going Native 2013.

Просто переходьте до приблизно 25 мільйонів у цьому відео . (Рекомендую переглянути все відео насправді, але це переходить до матеріалів про збирання сміття.)

Коли у вас є дійсно чудова мова, яка дозволяє легко (і безпечно, і передбачувано, і легко читати, і легко навчати) поводитися з предметами та цінностями прямо, уникаючи (явного) використання купи, тоді ви навіть не хочете збирати сміття.

З сучасним C ++ та речей, які ми маємо в C ++ 11, збирання сміття більше не бажане, за винятком обмежених обставин. Насправді, навіть якщо хороший сміттєзбірник вбудований в один із головних компіляторів С ++, я думаю, що він буде використовуватися не дуже часто. Уникнути ГК буде простіше , а не важче.

Він показує цей приклад:

void f(int n, int x) {
    Gadget *p = new Gadget{n};
    if(x<100) throw SomeException{};
    if(x<200) return;
    delete p;
}

Це небезпечно для C ++. Але це також небезпечно на Яві! У C ++, якщо функція повертається рано, deleteвиклик ніколи не буде викликаний. Але якщо у вас було повне збирання сміття, як, наприклад, на Java, ви просто отримуєте припущення про те, що об’єкт буде знищено "в якийсь момент майбутнього" ( Оновлення: ще гірше, що це робить Java. необіцяють зателефонувати до фіналізатора ніколи - його, можливо, ніколи не називатимуть). Це недостатньо добре, якщо гаджет містить відкриту ручку файлу, або з'єднання з базою даних, або дані, які ви завантажили для запису в базу даних пізніше. Ми хочемо знищити гаджет, як тільки він буде закінчений, щоб звільнити ці ресурси якнайшвидше. Ви не хочете, щоб ваш сервер баз даних боровся з тисячами підключень до бази даних, які більше не потрібні - він не знає, що ваша програма закінчена.

То яке рішення? Є кілька підходів. Очевидний підхід, який ви використовуєте для переважної більшості об'єктів, це:

void f(int n, int x) {
    Gadget p = {n};  // Just leave it on the stack (where it belongs!)
    if(x<100) throw SomeException{};
    if(x<200) return;
}

Для введення потрібно менше символів. Це не заважає new. Це не вимагає, щоб ви вводили Gadgetдвічі. Об'єкт знищується в кінці функції. Якщо це те, що ви хочете, це дуже інтуїтивно. Gadgets поводяться так само, як intі double. Прогнозований, легкий для читання, простий у навчанні. Все - це «цінність». Іноді велике значення, але значення легше навчати, оскільки у вас немає цієї дії "на відстані", яку ви отримуєте за допомогою покажчиків (або посилань).

Більшість створених вами об'єктів використовуються лише у функції, яка їх створила, і, можливо, передається як вхід до дочірніх функцій. Програмісту не слід думати про "управління пам'яттю" при поверненні об'єктів або іншим чином обмінюватися об'єктами на широко відокремлених частинах програмного забезпечення.

Сфера застосування та термін експлуатації важливі. Більшу частину часу простіше, якщо термін експлуатації такий же, як і область застосування. Це легше зрозуміти і легше вчити. Коли ви хочете іншого життя, слід чітко читати код, який ви це робите, наприклад, використовуючи shared_ptr. (Або повернення (великих) об'єктів за значенням, використання семантики переміщення або unique_ptr.

Це може здатися проблемою ефективності. Що робити, якщо я хочу повернути гаджет foo()? Семантика руху C ++ 11 полегшує повернення великих об'єктів. Просто напишіть, Gadget foo() { ... }і це буде просто працювати, і працювати швидко. Вам не потрібно возитися із &&собою, просто поверніть речі за значенням, і мова часто зможе зробити необхідні оптимізації. (Ще до C ++ 03 компілятори зробили надзвичайно гарну роботу, уникаючи зайвого копіювання.)

Як Струструп говорив в іншому відео (перефразовуючи): "Тільки комп'ютерний науковець наполягатиме на копіюванні об'єкта, а потім знищенні оригіналу. (Сміється аудиторія). Чому б просто не перемістити об'єкт безпосередньо на нове місце? Це те, що люди (не комп'ютерні) очікують ".

Коли ви можете гарантувати лише одну копію об’єкта, зрозуміти термін експлуатації об’єкту набагато простіше. Ви можете вибрати, яку політику життя ви хочете, і збирання сміття є, якщо хочете. Але коли ви зрозумієте переваги інших підходів, ви побачите, що збирання сміття знаходиться внизу списку налаштувань.

Якщо це не працює для вас, ви можете використовувати unique_ptr, або якщо це неможливо, shared_ptr. Добре написаний C ++ 11 коротший, легший для читання та простіший у навчанні, ніж багато інших мов, що стосується управління пам'яттю.


1
GC слід використовувати лише для об'єктів, які не набувають ресурсів (тобто просять інші організації робити речі від їх імені "до подальшого повідомлення"). Якщо Gadgetне вимагає нічого іншого робити щось від свого імені, оригінальний код був би абсолютно безпечним на Java, якби безглузду deleteзаяву (для Java) було видалено.
supercat

@supercat цікаві об’єкти із нудними деструкторами. (Я не визначив "нудно", але в основному деструктори, які ніколи не потрібно викликати, крім звільнення пам'яті). Можливо, окремий компілятор може поводитися shared_ptr<T>спеціально, коли Tце "нудно". Він може вирішити фактично не керувати лічильником повторів для цього типу, а замість цього використовувати GC. Це дозволило б використовувати GC без розробника, щоб цього помітити. A shared_ptrможна просто розглядати як покажчик GC, для відповідного T. Але в цьому є обмеження, і це зробило б багато програм повільнішими.
Аарон Макдейд

Система хорошого типу повинна мати різні типи для об'єктів купи, керованих GC та RAII, оскільки деякі схеми використання дуже добре працюють з одним і дуже погано з іншим. В .NET або Java, заяву string1=string2;буде виконувати дуже швидко , незалежно від довжини рядка (це не в буквальному сенсі не більше ніж навантаження регістра і реєстрація магазину), і не вимагає будь - яких блокування , щоб гарантувати , що якщо наведене вище твердження виконуються в той час як string2IS записане, string1буде містити або старе значення, або нове значення, без не визначеного поведінки).
supercat

У програмі C ++ присвоєння запиту shared_ptr<String>вимагає великої кількості поза кадрів і синхронізація Stringможе поводитися дивно, якщо змінна читається та записується одночасно. Випадки, коли хочеться Stringодночасно писати та читати , не є надзвичайно поширеними, але вони можуть виникнути, якщо, наприклад, якийсь код хоче зробити доступні звіти про стан іншим потокам. У .NET та Java такі речі просто «працюють».
supercat

1
@curiousguy нічого не змінилося, якщо не вжити правильних заходів безпеки, Java все ще дозволяє викликати фіналізатор, як тільки конструктор закінчиться. Ось приклад із реального життя: « доопрацювати () викликає сильнодоступні об’єкти в Java 8 ». Висновок полягає в тому, що ніколи не використовувати цю функцію, що майже всі погоджуються на історичну помилку дизайну мови. Коли ми дотримуємось цієї поради, мова надає детермінізм, який ми любимо.
Холгер

11

Тому що сучасний C ++ не потребує збору сміття.

Відповідь на питання Бжарне Струструпа з цього питання говорить :

Я не люблю сміття. Мені не подобається засмічення. Мій ідеал - усунути потребу у сміттєзбірнику, не виробляючи сміття. Це зараз можливо.


Ситуація щодо коду, написаного цими днями (C ++ 17 та дотримуючись офіційних Основних правил ), є наступною:

  • Більшість кодів власності на пам'ять є в бібліотеках (особливо в тих, що надають контейнери).
  • Найчастіше використання коду, що включає власність на пам'ять, відповідає схемі RAII , тому розподіл проводиться на побудову та делокацію при знищенні, що відбувається при виході з області, у якій щось було виділено.
  • Ви прямо не розподіляєте та не передаєте пам'ять безпосередньо .
  • Сирі покажчики не володіють пам’яттю (якщо ви дотримувались інструкцій), тому ви не можете просочитися, передаючи їх навколо.
  • Якщо вам цікаво, як ви збираєтеся передавати початкові адреси послідовностей значень в пам'яті - ви будете робити це з проміжком ; не потрібна сира вказівка.
  • Якщо вам дійсно потрібен власний "покажчик", ви використовуєте смарт-покажчики зі стандартними бібліотеками C ++ - вони не можуть просочуватися і є пристойно ефективними (хоча ABI може перешкоджати цьому). Крім того, ви можете передавати право власності через межі діапазону за допомогою "покажчиків власника" . Вони нечасті і повинні використовуватися явно; але коли вони прийняті - вони дозволяють приємно перевірити статичність на предмет витоків.

"О так? А як же ...

... якщо я просто напишу код так, як раніше писали C ++? "

Дійсно, ви можете просто проігнорувати всі вказівки та написати пропускний код програми - і він буде компілюватися та запускатися (і протікати), як завжди.

Але це не ситуація "просто не роби цього", коли розробник, як очікується, буде доброчесним і здійснює багато самоконтролю; просто не простіше писати невідповідний код, а також не швидше писати, а також не відрізняється кращою ефективністю. Поступово це також стане складніше писати, оскільки ви зіткнетеся зі зростаючою "невідповідністю імпедансу" тому, що відповідає і очікує відповідний код.

... якщо я reintrepret_cast? Або робити складну арифметику вказівника? Або інші такі хаки? "

Дійсно, якщо ви подумаєте про це, ви можете написати код, який зіпсує речі, незважаючи на те, що добре грати з настановами. Але:

  1. Ви б робили це рідко (з точки зору місць у коді, не обов'язково з точки зору частки часу виконання)
  2. Ви зробили б це лише навмисно, не випадково.
  3. Це буде виділятися в кодовій базі, що відповідає вказівкам.
  4. Це вид коду, в якому ви все одно обходите GC іншою мовою.

... розвиток бібліотеки? "

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


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

Це не означає, що GC не є цікавою проблемою для певних конкретних додатків, коли ви хочете використовувати спеціальні стратегії розподілу та дерозподілення. Для тих, хто хотів би користуватися спеціальним розподілом та дерозподіленням, а не загальною мовою на рівні мови.


Що ж, це робить (потрібен GC), якщо ви шліфуєте струни. знаю, бо мені довелося перейти на мови високого рівня, щоб впоратися. (Звичайно, ви також можете створити свій власний GC).
www-0av-Com

2
@ user1863152: Це випадок, коли користувацький алокатор буде корисним. Це все ще не вимагає інтегральної мови GC ...
einpoklum,

to einpoklum: вірно. Це просто кінь на курси. Моєю вимогою було обробляти динамічно мінливі галони інформації про пасажирських перевезень. Захоплюючий предмет .. Дійсно зводиться до філософії програмного забезпечення.
www-0av-Com

GC, як виявили світ Java та .NET, нарешті, має величезну проблему - вона не масштабується. Коли у вас в пам'яті є мільярди живих об’єктів, як у нас сьогодні, з будь-яким нетривіальним програмним забезпеченням, вам доведеться почати писати код, щоб приховати речі з GC. GC в Java та .NET - це тягар.
Zach Saw

10

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

Ніщо не заважає тобі скористатися якоюсь формою розумних покажчиків, які прив’язані до сторонніх механізмів збору сміття. Здається, я пам'ятаю, що Microsoft робив щось подібне з COM, і це не вдалося.


2
Я не думаю, що GC вимагає VM. Компілятор може додати код до всіх операцій з покажчиком для оновлення глобального стану, тоді як окремий потік працює у фоновому режимі, видаляючи об’єкти за потребою.
користувач83255

3
Я згоден. Вам не потрібна віртуальна машина, але друге, коли ви починаєте щось керувати своєю пам’яттю для вас, як на задньому плані, я вважаю, що ви залишили фактичні «електричні дроти» і маєте своєрідну ситуацію з ВМ.
Урі


4

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

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

Цікавою вигадкою багатьох фреймворків, зібраних сміттям, є те, що посилання на об'єкт не визначається бітовими шаблонами, що містяться в них, а залежністю між бітами, що містяться в посиланні на об'єкт, та іншою інформацією, що зберігається в іншому місці. У C і C ++, якщо бітовий шаблон, що зберігається в покажчику, ідентифікує об'єкт, цей бітовий шаблон буде ідентифікувати цей об'єкт, поки об'єкт явно не буде знищений. У типовій системі GC об'єкт може бути представлений бітовою схемою 0x1234ABCD в один момент часу, але наступний цикл GC може замінити всі посилання на 0x1234ABCD з посиланням на 0x4321BABE, після чого об'єкт буде представлений останнім шаблоном. Навіть якби відобразити бітовий візерунок, пов’язаний з посиланням на об'єкт, а потім пізніше його прочитати з клавіатури,


Це справді хороший момент, я нещодавно вкрав кілька біт у своїх покажчиків, бо в іншому випадку було б нерозумно пропущено кеш-пам'ять.
Перехожий

@PasserBy: Цікаво, скільки програм, які використовують 64-бітні покажчики, отримали б більше користі від використання масштабованих 32-бітних покажчиків як посилань на об'єкти, або ж збереження майже всього в адресному просторі 4GiB та використання спеціальних об'єктів для зберігання / отримання даних з високого рівня -швидше зберігання поза? У машинах достатньо оперативної пам’яті, що споживання оперативної пам’яті 64-бітних покажчиків може не мати значення, за винятком того, що вони збивають кеш у два рази більше кешу, ніж 32-бітні вказівники.
supercat

3

Вся технічна розмова надмірно ускладнює концепцію.

Якщо ви помістите GC у C ++ для всієї пам'яті автоматично, тоді розгляньте щось на зразок веб-браузера. Веб-браузер повинен завантажити повний веб-документ І запустити веб-скрипти. Ви можете зберігати змінні веб-скриптів у дереві документів. У BIG-документі в браузері з відкритою безліччю вкладок, це означає, що кожного разу, коли GC повинен виконати повну колекцію, він також повинен сканувати всі елементи документа.

На більшості комп'ютерів це означає, що виникнуть помилки PAGE PUST. Отже, головна причина відповіді на питання полягає в тому, що ПОСЛУГИ СТОРІНКИ відбудуться. Ви будете знати це, коли ваш ПК починає робити багато доступу до диска. Це тому, що GC повинен торкнутися багато пам'яті, щоб довести недійсні покажчики. Якщо у вас є добросовісний додаток, що використовує багато пам'яті, сканування всіх об'єктів скасовує кожну колекцію через ПОЛОЖЕННЯ СТОРІНКИ. Помилка сторінки - це коли віртуальна пам'ять потребує повернення в оперативну пам’ять з диска.

Тож правильне рішення - розділити додаток на частини, на які потрібен GC, та на частини, які цього не роблять. У випадку з вищенаведеним прикладом веб-браузера, якщо дерево документів було виділено з malloc, але javascript запускався з GC, то кожен раз, коли GC натискає в нього, сканує лише невелику частину пам'яті та всі ЗАПАСНІ вихідні елементи пам'яті для дерево документа не потрібно повертати на сторінку.

Щоб далі зрозуміти цю проблему, знайдіть віртуальну пам’ять та те, як вона реалізована в комп’ютерах. Вся справа в тому, що програмі доступно 2 Гб, коли не дуже багато ОЗУ. На сучасних комп'ютерах з 2 ГБ оперативної пам’яті для системи 32BIt це не така проблема, якщо працює лише одна програма.

В якості додаткового прикладу розглянемо повну колекцію, яка повинна простежити всі об'єкти. Спочатку слід сканувати всі об'єкти, доступні через коріння. По-друге, скануйте всі об'єкти, видимі на кроці 1. Потім скануйте деструктори, що очікують. Потім знову перейдіть на всі сторінки та вимкніть усі невидимі об’єкти. Це означає, що багато сторінок можуть бути замінені та повернені кілька разів.

Тож моя відповідь коротко полягає в тому, що кількість СТОРІНКОВИХ ПОМИЛЬ, які виникають в результаті торкання всієї пам'яті, викликає нездійсненність повного GC для всіх об'єктів програми, і тому програміст повинен розглядати GC як допомогу для таких речей, як сценарії і робота з базами даних, але робити звичайні речі з ручним управлінням пам'яттю.

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


3

КОРОТКИЙ ВІДПОВІДЬ: Ми не знаємо, як зробити сміття ефективно (з незначними витратами часу та місця) та правильно весь час (у всіх можливих випадках).

ДОВГИЙ ВІДПОВІДЬ: Подібно до C, C ++ - це системна мова; це означає, що він використовується під час написання системного коду, наприклад, операційної системи. Іншими словами, C ++ розроблений так само, як і C, з найкращою можливою продуктивністю в якості основної цілі. Стандарт мови не додасть жодної функції, яка може перешкоджати досягненню мети продуктивності.

Це призупиняє питання: Чому збирання сміття перешкоджає роботі? Основна причина полягає в тому, що, коли мова йде про впровадження, ми [комп'ютерні науковці] не знаємо, як зробити сміття з мінімальними накладними витратами, для всіх випадків. Отже, компілятор C ++ і система виконання не може постійно ефективно виконувати збирання сміття. З іншого боку, програміст C ++ повинен знати його дизайн / реалізацію, і він найкращий чоловік, який вирішить, як найкраще робити сміття.

Нарешті, якщо контроль (обладнання, деталі тощо) та продуктивність (час, простір, потужність тощо) не є основними обмеженнями, то C ++ не є інструментом запису. Інша мова може слугувати краще і запропонувати більше [прихованого] режиму виконання з необхідними накладними витратами.


3

Коли ми порівнюємо C ++ з Java, ми бачимо, що C ++ не розроблявся з неявним збиранням сміття, тоді як Java був.

Наявність таких речей, як довільні покажчики в C-Style, є поганим не тільки для GC-реалізацій, але й знищить відсталу сумісність для великої кількості С ++ - застарілого коду.

На додаток до цього, C ++ - це мова, яка призначена для роботи як автономного виконуваного файлу, а не складного середовища виконання.

Загалом: Так, можливо, можна додати збирання сміття до C ++, але заради наступності краще цього не робити.


1
Звільнення пам'яті та запущені деструктори - занадто повністю окремі проблеми. (У Java немає деструкторів, що є титаном PITA.) GC звільняє пам'ять, не запускає dtors.
допитливий

0

В основному з двох причин:

  1. Тому що він не потрібен (IMHO)
  2. Тому що це майже несумісне з RAII, що є наріжним каменем C ++

C ++ вже пропонує керування пам'яттю вручну, розподіл стеків, RAII, контейнери, автоматичні вказівники, смарт-покажчики ... Цього має бути достатньо. Збирачі сміття призначені для ледачих програмістів, які не хочуть витрачати 5 хвилин на роздуми над тим, хто повинен володіти якими об’єктами чи коли ресурси повинні бути звільнені. Це не так, як ми робимо речі в C ++.


Існує численні (новіші) алгоритми, які по суті важко реалізувати без збору сміття. Час рухався. Інновації також походять від нових розумінь, які добре відповідають (сміття) мовами високого рівня. Спробуйте підкріпити будь-яке з них на G + C ++, ви помітите задирки на дорозі. (Я знаю, що я повинен навести приклади, але я зараз поспішаю. Вибачте. Одне, про що я можу подумати, зараз обертається навколо стійких структур даних, де підрахунок посилань не буде працювати.)
BitTickler

0

Введення збору сміття насправді є зміною парадигми від низького рівня до високого рівня.

Якщо ви подивитеся на те, як обробляються рядки мовою зі збиранням сміття, ви побачите, що ТІЛЬКИ вони дозволяють виконувати функції маніпулювання рядками високого рівня і не дозволяють двійковий доступ до рядків. Простіше кажучи, усі функції рядків спочатку перевіряють покажчики, щоб побачити, де знаходиться рядок, навіть якщо ви лише малюєте байт. Отже, якщо ви робите цикл, який обробляє кожен байт у рядку мовою зі збиранням сміття, він повинен обчислити базове місце плюс зміщення для кожної ітерації, оскільки він не може знати, коли рядок перемістився. Тоді вам доведеться думати про купи, стеки, нитки тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.