Які складності програмування без керування пам'яттю?


24

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

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


3
Будь ласка, розгорніть, щоб сказати нам, як на ваше запитання не відповідає стаття у Вікіпедії про збирання одягу, а точніше розділ про її переваги
yannis

Іншою перевагою є безпека, наприклад, перевиконання буфера є високоефективною, а багато інших вразливих місць безпеки виникають внаслідок управління пам'яттю.
StuperUser

7
@StuperUser: Це не має нічого спільного з походженням пам'яті. Ви можете буферизувати обсяг пам'яті, що надійшла від GC, просто добре. Те, що мови GC зазвичай перешкоджають цьому, є ортогональним, а мови, які менше ніж на тридцять років відстають від технології GC, ви порівнюєте їх, також пропонують захист від переповнення буфера.
DeadMG

Відповіді:


29

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

Смішно, як з часом змінюється визначення "низького рівня". Коли я вперше вчився програмувати, будь-яка мова, що забезпечувала стандартизовану модель купи, яка робить можливим простий шаблон розподілу / вільного, вважалася справді високим рівнем. У програмуванні низького рівня вам доведеться самостійно відслідковувати пам'ять (не розподіли, а самі місця пам'яті!), Або записувати власний розподільник купи, якщо ви відчуваєте справді фантазію.

Сказавши це, взагалі немає нічого страшного або «складного» в цьому. Пам’ятаєте, коли ви були дитиною, і мама сказала вам, щоб прибрати свої іграшки, коли ви закінчите грати з ними, що вона не ваша покоївка і не збирається прибирати вашу кімнату для вас? Управління пам'яттю просто цей самий принцип застосовується до коду. (GC, як з покоївкою , яка буде прибирати за вами, але вона дуже ледачим і трохи неосвічені.) Принцип його простий: Кожна змінна в коді має один і тільки один власник, і це відповідальність цього власника звільнити пам'ять змінної, коли вона більше не потрібна. ( Принцип єдиної власності) Для цього потрібен один виклик за розподіл, і існує кілька схем, які автоматизують право власності та очищення так чи інакше, так що вам навіть не потрібно писати цей виклик у свій власний код.

Збір сміття повинен вирішити дві проблеми. На одній із них вона незмінно справляє дуже погану роботу, і залежно від впровадження може чи не може справитись з іншою. Проблеми полягають у витоку пам’яті (утримування пам’яті після того, як ви закінчите її) та вивішування посилань (звільнення пам’яті до того, як ви закінчите її.) Давайте розглянемо обидві проблеми:

Незвідане посилання: Спершу обговоріть це, бо це справді серйозно. У вас є два покажчики на один об’єкт. Ви звільните одну з них, а іншої не помітите. Потім в якийсь пізній момент ви намагаєтесь прочитати (або написати або безкоштовно) другий. Виникає не визначена поведінка. Якщо ви цього не помічаєте, ви можете легко зіпсувати пам'ять. Збір сміття повинен унеможливити цю проблему, гарантуючи, що нічого не буде звільнено, поки всі посилання на нього не зникнуть. Повністю керованою мовою це майже працює, поки вам не доведеться мати справу з зовнішніми, некерованими ресурсами пам'яті. Потім повертаємося до площі 1. А мовою, якою не керувати, все ще складніше. (Тикатися по Мозіллі '

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

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

Я, мабуть, зіпсується прихильниками GC, якщо я спробую сказати щось подібне, тому дозвольте мені пояснити. Пам’ятайте, що визначення витоку пам'яті утримується для виділеної пам’яті, коли вона більше не потрібна. Крім відсутності посилань на щось, ви можете також просочити пам'ять, маючи непотрібне посилання на нього, наприклад, тримаючи його в контейнерному об'єкті, коли ви повинні його звільнити. Я бачив деякі витоки пам’яті, спричинені цим, і їх дуже важко відстежити, чи є у вас GC чи ні, оскільки вони містять ідеально правильну посилання на пам’ять і немає чітких «помилок» інструментів налагодження для улов. Наскільки я знаю, не існує автоматизованого інструменту, який дозволяє вловлювати цей тип витоку пам'яті.

Тож збирач сміття стосується лише різноманітних витоків пам’яті без посилань, адже це єдиний тип, з яким можна вирішуватись автоматизовано. Якби він міг переглядати всі ваші посилання на все і звільнити кожен об'єкт, як тільки на нього вказують нульові посилання, було б ідеально, принаймні, що стосується проблеми без посилань. Робити це в автоматизованому порядку називається підрахунком довідок, і це може бути зроблено в деяких обмежених ситуаціях, але у нього є свої проблеми для вирішення. (Наприклад, об'єкт A, що містить посилання на об'єкт B, який містить посилання на об'єкт A. У схемі підрахунку посилань жоден об'єкт не може бути звільнений автоматично, навіть якщо немає зовнішніх посилань ні на A, ні на B.) сміттєзбірники використовують трасуваннянатомість: Почніть з набору об'єктів, які добре знають, знайдіть усі об'єкти, на які вони посилаються, знайдіть усі об'єкти, на які вони посилаються, і так далі рекурсивно, поки ви не знайдете все. Що б не знайти в процесі відстеження, це сміття і його можна викинути. (Зрозуміло, що для цього потрібно успішно, потрібна керована мова, яка встановлює певні обмеження для системи типів, щоб гарантувати, що збирач сміття завжди може визначити різницю між посиланням та деяким випадковим фрагментом пам'яті, який має вигляд вказівника.)

Існує дві проблеми з трасуванням. По-перше, це повільно, і, хоча це відбувається, програма повинна бути більш-менш призупинена, щоб уникнути перегонів. Це може призвести до помітних ікетів виконання, коли програма має взаємодіяти з користувачем, або до зменшення продуктивності в серверній програмі. Це можна пом'якшити різними методами, такими як розбиття виділеної пам’яті на «покоління» за принципом, що якщо виділення не буде зібрано вперше при спробі, швидше за все, воно буде триматися на деякий час. І .NET Framework, і JVM використовують покоління сміттєзбірників.

На жаль, це підсилює другу проблему: пам'ять не звільняється, коли ви закінчите її. Якщо трасування не проходить одразу після того, як ви закінчите об'єкт, він буде триматися навколо наступного сліду або навіть довше, якщо він пройде через перше покоління. Насправді одне з найкращих пояснень, який я бачив, збирач сміття .NET пояснює, що для того, щоб зробити процес максимально швидким, GC повинен відкласти збір на стільки, на скільки це можливо! Тож проблема протікання пам’яті «вирішується» досить химерно, просочуючи якомога довше пам’яті! Це те, що я маю на увазі, коли кажу, що GC перетворює кожен розподіл у витік пам'яті. Насправді, немає гарантії, що будь-який даний об’єкт коли-небудь буде зібраний.

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

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

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

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

Коли ви справді дивитесь на це, збирання сміття може або не може справитись із запобіганням звисаючих посилань, і, як правило, погана робота з усунення витоків пам'яті. Єдиною її чеснотою, насправді, є не само вивезення сміття, а побічний ефект: він забезпечує автоматизований спосіб виконувати ущільнення купи. Це може запобігти проблемі таємниці (виснаження пам’яті через фрагментацію купи), яка може вбивати програми, які постійно працюють протягом тривалого часу та мають високий ступінь збиття пам’яті, а ущільнення купи майже неможливо без збирання сміття. Однак будь-який хороший розподільник пам'яті в ці дні використовує відра, щоб мінімізувати фрагментацію, а це означає, що фрагментація справді стає проблемою лише в екстремальних обставинах. Для програми, в якій фрагментація купи, ймовірно, буде проблемою, вона " s доцільно використовувати ущільнювач сміття. Але ІМО у будь-якому іншому випадку використання сміття - це передчасна оптимізація, і існують кращі рішення проблем, які він "вирішує".


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

3
Я хотів би зазначити, що так, ГК, як правило, "просочують" пам'ять (принаймні на деякий час), але це не проблема, оскільки вона буде збирати пам'ять, коли розподільник пам'яті не може виділити пам'ять до збирання. З мовою, яка не є GC, витік завжди залишається витоком, тобто у вас фактично може не вистачати пам'яті через занадто багато не зібраної пам'яті. "Збір сміття - це передчасна оптимізація" ... GC не є оптимізацією і не розроблялася з цього приводу. Інакше хороша відповідь.
Томас Едінг

7
@ThomasEding: GC, безумовно, є оптимізацією; він оптимізує для мінімальних зусиль програміста за рахунок продуктивності та різних інших показників якості програми.
Мейсон Уілер

5
Смішно, що ви в один момент вказуєте на трекер помилок Mozilla, тому що Mozilla дійшов зовсім іншого висновку. У Firefox було і продовжує виникати незліченна кількість питань безпеки, пов’язаних з помилками управління пам’яттю. Зауважте, що мова йде не про те, як легко виправити виявлену помилку --- зазвичай шкода вже робиться до моменту, коли розробники дізнаються про проблему. Mozilla фінансує мову програмування Rust саме для запобігання введенню таких помилок.

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

13

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

Пам'ятайте, коли люди думають про не-GC, вони думають mallocі free. Але це величезна логічна помилка - ви б порівнювали управління ресурсами, що не належать до GC початку 1970-х, з сміттєзбірниками кінця 90-х. Це, очевидно , досить несправедливо збирачі результаті порівняння сміття, що були у вжитку , коли mallocі freeбули розроблені були занадто повільними , щоб запустити будь-яку змістовну програму, якщо я правильно пам'ятаю. Порівнювати щось із нечітко еквівалентним періодом часу, наприклад unique_ptr, є набагато більш значущим.

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

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

Редагувати: Багато речей, які ви згадуєте, не мають нічого спільного з GC. Ви плутаєте управління пам’яттю та орієнтацію на об’єкти. Дивіться, ось що: Якщо ви програмуєте в цілком некерованій системі, як-от C ++, ви можете перевіряти стільки меж, скільки вам подобається, і стандартні класи контейнерів пропонують це. Немає нічого в ГК щодо перевірки меж, наприклад, або сильного набору тексту.

Проблеми, які ви згадуєте, вирішуються об'єктно-орієнтованою, а не ГК. Походження пам'яті масиву та переконання, що ви не пишете поза нею, є ортогональними поняттями.

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


Розширене обговорення тут було очищено: якщо кожен може взяти його до чату, щоб обговорити цю тему далі, я дуже вдячний.

@DeadMG, ти знаєш, що повинен робити комбінатор? Слід поєднувати. За визначенням, комбінатор - це функція без вільних змінних.
SK-логіка

2
@ SK-логіка: Я міг би вибрати це реалізовувати виключно за шаблоном і не мати змінних членів. Але тоді ви не зможете пройти у закриттях, що істотно обмежує його корисність. Хочете піти на чат?
DeadMG

@DeadMG, визначення є кристально чистим. Без вільних змінних. Я вважаю будь-яку мову "достатньо функціональною", якщо можна визначити Y-комбінатор (правильно, не ваш спосіб). Велике "+" - це якщо це можливо визначити за допомогою комбінаторів S, K і I. Інакше мова недостатньо виразна.
SK-логіка

4
@ SK-логіка: Чому ти не зайдеш на чат , як запитав добрий модератор? Крім того, Y-комбінатор - це Y-комбінатор, він виконує роботу або ні. Версія Haskell Y-комбінатора в основному точно така ж, як і ця, просто виражений стан приховано від вас.
DeadMG

11

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

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

Збір сміття - це просто необхідний інструмент для мов, які мають ще одну, більш благородну мету: ставитися до всього як до вказівника на об’єкт і в той же час приховувати від програміста факт, що він є вказівником, щоб програміст не міг вчинити самогубство шляхом спроб арифметики вказівника тощо. Все, що є об'єктом, означає, що мови GCed повинні виділяти об'єкти набагато частіше, ніж мови, які не є GCed, а це означає, що якщо вони покладуть тягар розгляду цих об'єктів на програміста, вони будуть надзвичайно непривабливими.

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

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


4

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

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


1
@ Mason Wheeler, навіть C ++ реалізує дуже обмежену форму закриттів. Але це майже не належне, загальноприйнятне закриття.
SK-логіка

1
Ви помиляєтеся. Жоден GC не може захистити вас від того, що ви не можете посилатися на змінні стека. І це смішно - в C ++ ви також можете скористатися підходом "Скопіювати покажчик на динамічно розподілену змінну, яка буде відповідним чином і автоматично знищена".
DeadMG

1
@DeadMG, ви не бачите, що ваш код просочується суб'єктами низького рівня через будь-який інший рівень, який ви будуєте на вершині?
SK-логіка

1
@ SK-Logic: Гаразд, у нас є термінологічна проблема. Яке ваше визначення "реального закриття", і що вони можуть зробити, що закриття Delphi не може? (І включаючи все, що стосується управління пам’яттю, у ваше визначення - це переміщення цілей. Поговоримо про поведінку, а не про деталі реалізації.)
Мейсон Уілер

1
@ SK-Logic: ... а чи є у вас приклад того, що можна зробити за допомогою простих нетипізованих лямбда-закриттів, яких закриття Delphi не може виконати?
Мейсон Уілер

2

Дійсно, управління власною пам’яттю є лише ще одним потенційним джерелом помилок.

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


3
Пропущений free- не найгірше. Ранній freeнабагато руйнівніший.
herby

2
І подвійний free!
Quant_dev

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

1
Це помилка. Ви порівнюєте "початок 1970 року" з "кінцем 1990 року". ГКС , які існували в той час , на якому mallocі freeбув шлях без GC , щоб йти було значно надто повільно , щоб бути корисними для чого - небудь. Вам потрібно порівнювати це із сучасним підходом, що не стосується GC, як RAII.
DeadMG

2
@DeadMG RAII - це не ручне управління пам'яттю
quant_dev

2

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


1

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

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

Без GC вам слід відстежувати весь життєвий цикл виділеної пам'яті. Кожен раз, коли ви передаєте адресу вгору або вниз від підпрограми, яка її створила, у вас є позакерована посилання на цю пам'ять. У погані старі часи, навіть маючи лише одну нитку, рекурсія та операційна система (Windows NT) не дозволили мені контролювати доступ до виділеної пам’яті. Мені довелося встановити безкоштовний метод у власній системі розподілу, щоб утримати блоки пам'яті протягом певного часу, поки всі посилання не очистилися. Час утримання був чистим здогадком, але він працював.

Тож це єдина користь від GC, про яку я знаю, але я не міг би жити без неї. Я не думаю, що будь-який тип ООП пролетить без нього.


1
У Delphi та C ++ вони були дуже успішними, як мови OOP без будь-якої GC. Все, що потрібно для запобігання "поза контрольним посиланням" - це трохи дисципліни. Якщо ви розумієте принцип єдиної власності (див. Мою відповідь), проблеми, про які ви тут говорите, стають тотальними непроблемами.
Мейсон Уілер

@MasonWheeler: Коли настає час звільнення власника, він повинен знати всі місця, на які посилаються його об'єкти. Утримання цієї інформації та використання її для видалення посилань виглядає для мене дуже великою роботою. Я часто знаходив, що посилання ще не можна було повністю очистити. Я повинен був позначити власника як видаленого, а потім періодично вводити його в життя, щоб побачити, чи може він безпечно звільнитися. Я ніколи не використовував Delphi, але для невеликої жертви в ефективності виконання C # / Java дав мені великий приріст у часі розробки за C ++. (Не все завдяки GC, але це допомогло.)
RalphChapin

1

Фізичні витоки

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

Виходячи з кінця С, що робить управління пам'яттю приблизно максимально ручним і яскраво вираженим, щоб ми порівнювали крайнощі (C ++ здебільшого автоматизує управління пам’яттю без GC), я б сказав «не дуже» в сенсі порівняння з GC, коли він приходить до витоків . Новачок, а іноді навіть і професіонал, може забути написати freeдля даної задачі malloc. Це однозначно буває.

Однак є такі інструменти, як valgrindвиявлення витоку, які негайно помітять при виконанні коду, коли / де такі помилки трапляються до точного рядка коду. Коли це інтегровано в CI, злиття таких помилок стає майже неможливим, і їх легко виправити, як пиріг. Тож ніколи не буває великої справи в будь-якій команді / процесі з розумними стандартами.

Зрозуміло, можуть бути якісь екзотичні випадки страти, які летять під радаром тестування, де freeне вдалося викликати, можливо, наткнувшись на неясну зовнішню помилку введення, наприклад, пошкоджений файл, і в цьому випадку, можливо, система просочується 32 байти чи щось. Я думаю, що це однозначно може статися навіть при досить хороших стандартах тестування та інструментах виявлення витоку, але також було б не так вже й критично, щоб витікати трохи пам'яті на щось, що майже ніколи не відбувається. Ми побачимо набагато більшу проблему, де ми можемо просочувати масивні ресурси навіть у загальних шляхах виконання нижче таким чином, що GC не може запобігти.

Це також важко, якщо щось не нагадує псевдо-форму GC (підрахунок посилань, наприклад), коли термін експлуатації об'єкта потрібно продовжити для певної форми відкладеної / асинхронної обробки, можливо, іншим потоком.

Показні покажчики

Справжня проблема з більш ручними формами управління пам’яттю не протікає для мене. Скільки власних додатків, написаних на C або C ++, ми знаємо про це, що насправді є прохідними? Чи протікає ядро ​​Linux? MySQL? CryEngine 3? Цифрові аудіо робочі станції та синтезатори? Чи протікає Java VM (вона реалізована в рідному коді)? Photoshop?

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

Питанням для мене завжди була безпека. Навіть коли ми freeпам’ятаємо через покажчик, якщо є якісь інші вказівники на ресурс, вони стануть звисаючими (недійсними) покажчиками.

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

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

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

Управління ресурсами: Збір сміття

Комплексне управління ресурсами - це нелегкий, ручний процес, незалежно від того. GC тут не може нічого автоматизувати.

Візьмемо приклад, де у нас є цей об’єкт, "Джо". На Джо посилається ряд організацій, членом яких він є. Щомісяця або близько того вони витягують членський внесок з його кредитної картки.

введіть тут опис зображення

У нас також є одне посилання на Джо, щоб контролювати його життя. Скажімо, нам, як програмістам, Джо більше не потрібен. Він починає нас докучати, і нам більше не потрібні ці організації, яким він належить витрачати свій час на боротьбу з ним. Тож ми намагаємось витерти його з лиця землі, видаливши його життєву орієнтир.

введіть тут опис зображення

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

введіть тут опис зображення

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

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

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

Управління ресурсами: Посібник

Тепер розглянемо альтернативу, де ми використовуємо вказівники на Джо та ручне управління пам’яттю, наприклад:

введіть тут опис зображення

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

введіть тут опис зображення

Тепер це нормально залишатиме нам вказівники повсюди, тож давайте видалимо покажчики Джо.

введіть тут опис зображення

... ого, ми знову зробили таку саму помилку і забули скасувати підписку на журнал Джо на підписку!

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

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

Реальний світ

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

Шанс, що хтось колись буде керувати ресурсами десь у цій кодовій базі, як правило, досить високий, і ця ймовірність збігається з GC або без нього. Основна відмінність полягає в тому, що станеться в результаті цієї помилки, що також впливає на те, як швидко ця помилка буде виявлена ​​та виправлена.

Краш проти витоку

Тепер який із них гірший? Негайний крах або мовчазна пам'ять просочилася, де Джо просто загадково затримується?

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

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

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

Слабка література

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

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

У будь-якому випадку, чи полегшує вам чи важче життя GC, залежить від того, наскільки важливо для Вашого програмного забезпечення уникати протікань і чи стосується він складного управління ресурсами такого роду чи ні.

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

У всякому разі, це відмінності між GC та ручним управлінням пам'яттю. Щоб відповісти на ваше безпосереднє запитання, я б сказав, що керування ручною пам’яттю є важким, але це має дуже мало спільного з витоками, і як GC, так і ручні форми управління пам'яттю все ще дуже складні, коли управління ресурсами нетривіальне. Можливо, GC має більш хитру поведінку, коли програма, здається, працює просто чудово, але витрачає все більше і більше ресурсів. Ручна форма є менш хитрою, але вона вийде з ладу і спалить великий час з помилками, як показана вище.


-1

Ось перелік проблем, з якими стикаються програмісти на C ++ під час роботи з пам'яттю:

  1. Проблема масштабування виникає у виділеній стеку пам’яті: термін її експлуатації не подовжується поза функцією, в якій вона була виділена. Є три основні рішення цієї проблеми: купа пам’яті та переміщення точки розподілу вгору в стеці виклику або виділення зсередини об’єктів .
  2. Проблема sizeof полягає в розподілі стеку та виділенні їх зсередини об'єкта та частково виділеної пам'яті. Куля пам'яті не може змінюватися під час виконання. Рішення - це масиви пам'яті, покажчики, бібліотеки та контейнери.
  3. Проблема порядку визначення - це при розподілі зсередини об'єктів: класи всередині програми повинні бути у правильному порядку. Рішення - це обмеження залежностей від дерева та впорядкування класів, а не використання прямих декларацій, покажчиків та накопичувальної пам’яті та використання оголошень вперед.
  4. Проблема "Внутрішня-Зовнішня" полягає у виділеній об'єктом пам'яті. Доступ до пам’яті всередині об’єктів розділено на дві частини, частина пам'яті знаходиться всередині об’єкта, а інша пам’ять знаходиться поза нею, і програмістам потрібно правильно вибрати використовувати композицію чи посилання на основі цього рішення. Рішення роблять рішення правильно, або вказівники та купують пам'ять.
  5. Проблема рекурсивних об'єктів полягає в виділеній пам'яті об'єктів. Розмір об'єктів стає нескінченним, якщо один і той же об'єкт розміщується всередині себе, а рішення - це посилання, купа пам'яті та покажчики.
  6. Проблема відстеження власності полягає у виділеній пам’яті купи, вказівник, що містить адресу виділеної пам’яті купи, повинен бути переданий від точки розподілу до точки розміщення. Рішення - це пам'ять, виділена стеком, об'єкт-розподілена пам'ять, auto_ptr, спільний_ptr, унікальний_ptr, контейнери stdlib.
  7. Проблема дублювання власності полягає у виділеній купі пам’яті: угоду розміщення можна зробити лише один раз. Рішення - це пам'ять, виділена стеком, пам'ять, виділена об'єктом, auto_ptr, shared_ptr, unique_ptr, контейнери stdlib.
  8. Проблема з нульовим вказівником полягає у виділеній купі пам’яті: покажчикам дозволено бути NULL, що призводить до краху багатьох операцій під час виконання. Рішення - це стекова пам'ять, об'єкт-розподілена пам'ять і ретельний аналіз ділянок купи та посилання.
  9. Проблема з витоком пам’яті полягає у виділеній купі пам’яті: Забуття видалити виклик для кожного виділеного блоку пам’яті. Рішення - це такі інструменти, як valgrind.
  10. Проблема переповнення стеку полягає у рекурсивних викликах функцій, які використовують пам'ять стека. Зазвичай розмір стека повністю визначається під час компіляції, за винятком рекурсивних алгоритмів. Неправильне визначення розміру стека ОС також часто викликає цю проблему, оскільки немає можливості виміряти необхідний розмір простору стеку.

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


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