Коли корисно змусити прибирати сміття?


135

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

Чи можете ви сказати мені, за яким сценарієм насправді є доброю чи розумною ідеєю змусити збирати сміття?

Я не прошу конкретних випадків на C #, а навпаки, всі мови програмування, які мають смітник. Я знаю, що ви не можете застосовувати GC на всіх мовах, як-от Java, але припустимо, що ви можете.


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

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

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

4
Я не бачу, як можна відповісти на це неспецифічним способом. Підходить для 32-бітних процесів, не стосується 64-бітних процесів. .NET JVM, а для найвищого класу
rwong

3
@DavidConrad ви можете примусити його в C #. Звідси питання.
Омега

Відповіді:


127

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

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

Єдина порада щодо колекції, яку я можу дати, - це ніколи не робити.

Якщо ви справді знаєте складні деталі GC, вам не знадобиться моя порада, тому це не має значення. Якщо ви ще не знаєте зі 100% впевненістю, це допоможе, і вам доведеться шукати в Інтернеті і знайти відповідь, як це: Вам не слід телефонувати в GC.Collect , або, як альтернатива: Вам слід дізнатися деталі того, як працює GC. всередині і зовні, і тільки тоді ви дізнаєтесь відповідь .

Є одне безпечне місце, яке має сенс використовувати GC.Collect :

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

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


Надуманий приклад все одно

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

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


Тож давайте поговоримо про деякі семантики та механізми в .NET GC ... або ..

Все, що я думаю, я знаю про .NET GC

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

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


Концепції GC

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

Об'єкти в Gen 0 купи пережили 0 колекцій, вони були нещодавно зроблені, так що нещодавно з моменту їх створення не відбулося жодної колекції. Об'єкти у вашій грі Gen 1 прожили один прохід колекції, а також об’єкти у вашій грі Gen 2 прожили два проходи колекції.

Тепер варто зазначити причину, згідно з якою вони кваліфікують ці конкретні покоління та розділи відповідно. .NET GC розпізнає лише ці три покоління, тому що колекція передач, що переходять на ці три купи, дещо відрізняється. Деякі об'єкти можуть вижити, колекція проходить тисячі разів. GC просто залишає їх на іншій стороні купи гена 2, немає сенсу розділяти їх більше ніде, оскільки вони насправді є Gen 44; колекційний пропуск на них такий самий, як у всіх у ген 2 купи.

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


Що в колекції

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

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

Тепер, маючи уявлення про 3 купи пам'яті, всі розділені на кількість пропусків колекції, які вони пережили, давайте поговоримо про те, чому існують ці розділи.


Колекція Gen 0

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


Колекція Gen 1

Будь 1, будучи об'єктами, які не потрапили до цієї дуже тимчасової категорії об'єктів, все ще може бути досить короткочасним, оскільки все-таки значна частина створених об'єктів довго не використовується. Тому Gen 1 також збирає досить часто, знову зберігаючи невелику купу, щоб колекції були швидкими. Однак припущення менше, тому що об'єкти тимчасовіші ніж Gen 0, тому вони збираються рідше, ніж Gen 0

Я скажу, що відверто не знаю технічних механізмів, які відрізняються між пропускним кодом Gen 0 і Gen 1, якщо вони є, крім частоти, яку вони збирають.


Колекція Gen 2

Gen 2 тепер повинен бути матір'ю всіх купи, правда? Ну так, це більш-менш правильно. Тут живуть усі ваші постійні об’єкти - об’єкт, наприклад, ваше Main()життя, і все, на що Main()посилається, тому що вони будуть вкорінені до тих пір, поки ви не Main()повернетеся в кінці процесу.

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


Велика купа об’єктів

Одним із прикладів додаткової поведінки в ген 2 є те, що він також збирає колекцію на великій купі об'єктів. До цього часу я повністю говорив про малу купі об’єктів, але час виконання .NET виділяє речі певного розміру окремій купі через те, що я назвав вище ущільненням. Для ущільнення потрібні переміщення об’єктів, коли колекції закінчуються на Малій купі об’єктів. Якщо в Gen 1 є живий об'єкт 10 Мб, це займе набагато більше часу, щоб завершити ущільнення після його збору, тим самим уповільнивши колекцію Gen 1. Таким чином, об'єкт 10 Мб виділяється на Велику групу об'єктів і збирається під час ген 2, який працює так рідко.


Фіналізація

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

Механізм, за допомогою якого фіналізатори це роблять, - це посилання безпосередньо в чергу завершення. Коли час виконання виділяє об'єкт з фіналізатором, він додає вказівник на цей об’єкт до черги фіналізації та блокує ваш об'єкт на місці (називається закріпленням), щоб ущільнення не перемістило його, що порушило б посилання черги на завершення. По мірі проходження колекції, зрештою, буде виявлено, що ваш об'єкт більше не має кореня GC, але доопрацювання має бути виконано до того, як його можна буде зібрати. Отже, коли об’єкт мертвий, колекція перемістить його посилання з черги на завершення та розмістить посилання на нього, що відомо як черга "FReachable". Потім колекція продовжується. В інший "недетермінований" час у майбутньому, окремий потік, відомий як потік Finalizer, буде проходити через чергу FReachable, виконуючи фіналізатори для кожного з об'єктів, на які посилається. Після її завершення черга з можливістю очищення порожня, і вона трохи перегорнула заголовок кожного об'єкта, що говорить, що вони не потребують доопрацювання (цей біт також можна перевернути вручнуGC.SuppressFinalizeщо є загальним у Dispose()методах), я також підозрюю, що він відкрутив об'єкти, але не цитуйте мене з цього приводу. Наступна колекція, яка з’явиться на будь-якій купі цього об’єкта, нарешті збирає її. Колекції Gen 0 навіть не звертають уваги на об’єкти, у яких потрібен біт для доопрацювання, він автоматично просуває їх, навіть не перевіряючи їх корінь. Укорінений об'єкт, який потребує доопрацювання в Gen 1, буде кинутий у FReachableчергу, але колекція нічого іншого не робить, тому він переходить у Gen 2. Таким чином, усі об'єкти, які мають фіналізатор, не роблять GC.SuppressFinalizeбуде зібрано у 2-му поколінні.


4
@FlorianMargaine так ... говорити про "GC" у всіх реалізаціях насправді не має сенсу ..
Джиммі Хоффа

10
tl; dr: замість цього використовуйте об'єктивні пули.
Роберт Харві

5
tl; dr: Для визначення часу / профілювання це може бути корисно.
kutschkem

3
@ Коли я прочитав вище опис механіки (наскільки я їх розумію), яка б це була користь, як ви це бачите? Ви прибираєте велику кількість об’єктів - у ДГУ (чи ЛОГ?)? Чи просто ви викликали паузи для цієї колекції для інших тем? Чи збільшила ця колекція лише в Gen 2 вдвічі більше об'єктів, ніж очищена? Чи колекція спричинила ущільнення на LOH (у вас це включено?)? Скільки груп GC у вас є і чи є ваш GC в режимі сервера чи настільного ПК? GC - фекальний льодовий берг, зрада - нижче вод. Просто тримайтеся чітко. Я не досить розумний, щоб зручно збирати.
Джиммі Хоффа

4
@RobertHarvey Об'єктні басейни теж не є срібною кулею. Покоління сміттєзбірника 0 покоління вже фактично є об'єктом пулу - зазвичай воно розміром підходить для найменшого рівня кешу, тому нові об'єкти, як правило, створюються в пам'яті, яка вже є в кеші. Тепер ваш об'єктний пул конкурує з розплідником GC за кеш, і якщо сума розплідників GC та вашого пулу більша за кеш, у вас, очевидно, буде пропущено кеш. І якщо ви плануєте використовувати паралелізм зараз, вам доведеться повторно впроваджувати синхронізацію та турбуватися про помилковий обмін.
Доваль

68

На жаль, там ніхто не деталізується, що таке випадки.

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

  • Тести різних рівнів. Ви хочете, щоб відомий керований стан купи починався, коли еталон починається, щоб GC не випадково запускався під час орієнтирів. При повторенні еталону ви хочете однакове число і кількість GC роботи в кожному повторенні.
  • Раптовий вивільнення ресурсів. Наприклад, закрити значне вікно GUI або оновити кеш (і тим самим випустити старий потенційно великий вміст кешу). GC не може виявити це, оскільки все, що ви робите, це встановлення посилання на null. Те, що у цих дітей-сиріт весь графік об'єкта, не легко виявити.
  • Випуск некерованих ресурсів, які просочилися . Це ніколи не повинно траплятися, звичайно, але я бачив випадки, коли третя сторона бібліотеки просочувала речі (наприклад, об'єкти COM). Розробник змушений був інколи викликати колекцію.
  • Інтерактивні програми, такі як ігри . Під час ігор мають дуже суворі часові бюджети на кадр (60 Гц => 16 мс на кадр). Щоб уникнути гикавок, вам потрібна стратегія боротьби з ГК. Однією з таких стратегій є максимально затримати G2 GC та примусити їх у відповідний час, наприклад, завантажувальний екран або зріз. ГК не може знати, коли найкращий такий момент.
  • Контроль затримки загалом. Деякі веб-додатки відключають GC та періодично запускають колекцію G2, вимикаючись із обертання балансира навантаження. Таким чином, затримка G2 ніколи не з’являється для користувача.

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

Наведені вище приклади націлені на послідовність та обмеженість використання пам'яті. У цих випадках індуковані ГК можуть мати сенс.

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

Зауважте, що вартість GC змінюється залежно від розміру купи та кількості посилань на купу. На невеликій купі ціна може бути дуже невеликою. Я бачив швидкість збору G2 з .NET 4,5 1-2 Гб / с у виробничому додатку з розміром купи 1 Гб.


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

3
+1 для другого до останнього абзацу. Деякі люди мають однакові настрої щодо компіляторів і швидко називають "передчасну оптимізацію". Зазвичай я їм кажу щось подібне.
Хонза Брабек

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

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

27

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

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


12

Одне, про що ніхто не згадував, це те, що, хоча Windows GC надзвичайно хороший, GC на Xbox - сміття (каламбур призначений) .

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


4

Збір сміття - це насамперед інструмент управління пам’яттю. Таким чином, смітники збиратимуть, коли буде тиск пам’яті.

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

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

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

RMI

Одним із конкретних прикладів цього є віддалене виклик методу Java. RMI - це бібліотека викликів віддалених процедур. Зазвичай у вас є сервер, який робить різні об’єкти доступними для використання клієнтами. Якщо сервер знає, що об’єкт не використовується жодними клієнтами, то цей об’єкт придатний для збору сміття.

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

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

Рішення в RMI полягає в тому, щоб клієнт періодично запускав збір сміття, навіть якщо у нього багато вільної пам'яті, щоб забезпечити швидке збирання об'єктів на сервері.


"У цих випадках може бути доцільним вручну викликати збирання сміття, коли ви знаєте, що використовується цінний ресурс, що не використовується в пам'яті" - якщо використовується ресурс, який не використовується в пам'яті, ви повинні використовувати usingблок або іншим чином викликати Closeметод для переконайтеся, що ресурс відкидається якнайшвидше. Покладатися на GC з очищення ресурсів, що не належать до пам’яті, є ненадійним і викликає всілякі проблеми (особливо з файлами, які потрібно заблокувати для доступу, тому вони можуть бути відкриті лише один раз).
Жуль

І як зазначено у відповіді, коли closeметод доступний (або ресурс можна використовувати з usingблоком), це правильний підхід. Відповідь конкретно стосується рідкісних випадків, коли ці механізми відсутні.
James_pic

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

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

2

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

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

Однак у деяких типах обробки даних ви знаєте більше, ніж GC. Наприклад, розгляньте програму, яка.

  • Дається список імен файлів у командному рядку
  • Обробляє один файл, а потім записує результат у файл результатів.
  • Під час обробки файлу створюється безліч взаємопов'язаних об'єктів, які неможливо зібрати, поки обробка файлу не завершиться (наприклад, дерево розбору)
  • Не підтримує стан відповідності між обробленими файлами .

Ви можете бути в змозі зробити випадок (після ретельної перевірки) , що ви повинні змусити повний збір сміття після того як ви обробку кожен файл.

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

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

Я б скоріше мав API збору сміття, коли я міг би давати йому підказки щодо такого типу речей без того, щоб змушувати GC себе.

Дивіться також " Підказки виступу Ріко Маріані "


2

Є кілька випадків, коли ви можете самостійно зателефонувати за допомогою gc ().

  • [ Деякі люди кажуть, що це не добре, оскільки це може просувати об’єкти до простору старшого покоління, що я згоден - це не дуже добре. Однак НЕ завжди вірно, що завжди знайдуться об'єкти, які можна просувати. Безумовно, що після цього gc()дзвінка дуже мало об’єктів залишається, не кажучи вже переміщених у простір старшого покоління ] Коли ви збираєтеся створити велику колекцію об'єктів і використовувати багато пам'яті. Ви просто хочете звільнити якомога більше місця, наскільки це можливо підготовка. Це просто здоровий глузд. Зателефонувавши gc()вручну, не буде зайвим перевірити графік еталонної частини тієї великої колекції об’єктів, які ви завантажуєте в пам'ять. Якщо коротко, якщо ви запускаєтесь, gc()перш ніж багато завантажувати в пам'ять, тоgc() Індукований під час навантаження відбувається менше, щонайменше, на один раз, коли завантаження починає створювати тиск пам'яті.
  • Після завантаження великої колекціївеликийоб'єктів, і ви навряд чи завантажите більше об'єктів у пам'ять. Коротше кажучи, ви переходите від створення фази до використання фази. Викликаючи gc()залежно від реалізації, пам'ять у використаній пам'яті буде ущільнена, що значно покращує локальність кешу. Це призведе до значного покращення продуктивності, якого ви не отримаєте від профілювання .
  • Подібно до першого, але з точки зору того, що якщо ви це зробите gc()і підтримка управління пам'яттю підтримує, ви створите набагато кращу безперервність для фізичної пам'яті. Це знову робить нову велику колекцію об'єктів більш безперервною та компактною, що, у свою чергу, покращує продуктивність

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

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

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

3
Ваша перша думка про нормальний час - це лише погана порада на мою думку. Висока ймовірність того, що нещодавно з’явилася колекція, тому ваша спроба знову зібрати просто збирається довільно просувати предмети для наступних поколінь, що майже завжди погано. Пізніші покоління мають колекції, що займають більше часу, збільшуючи їх розміри, щоб "звільнити якомога більше місця", просто це стає більш проблематичним. Плюс якщо ви збираєтесь підвищити тиск пам'яті з навантаженням, ви, швидше за все, почнете індукувати колекції, які будуть працювати повільніше, оскільки збільшився Gen1 / 2
Джиммі Хоффа

2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Якщо ви виділите тону об'єктів підряд, шанси вони вже ущільнені. У будь-якому випадку, збирання сміття може злегка перемістити їх. Так чи інакше, використання щільних структур даних, які не скачуться випадково в пам'яті, матиме більший вплив. Якщо ви використовуєте наївний список, пов’язаний з одним елементом на вузол, жодна кількість ручних хитрощів GC для цього не компенсує.
Доваль

2

Приклад із реального світу:

У мене був веб-додаток, який використовував дуже великий набір даних, які рідко змінювалися і до яких потрібно було отримати доступ дуже швидко (досить швидко для відповіді на натискання клавіші через AJAX).

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

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

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

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

Тому періодичні дзвінки GC.Collect()під час цього процесу збирання означали, що вся справа працювала безперебійно. Звичайно, я не вручну дзвонив GC.Collect()в інший час, коли програма працює.

Цей реальний випадок є хорошим прикладом вказівок, коли нам слід використовувати GC.Collect():

  1. Використовуйте порівняно рідкісний випадок, коли велика кількість об'єктів стає доступною для збору (мегабайти варто було доступно, і ця побудова графіків була дуже рідкісним випадком протягом життя програми (близько однієї хвилини на тиждень).
  2. Робіть це, коли втрата продуктивності відносно допустима; це сталося лише при запуску програми. (Ще один хороший приклад цього правила - між рівнями під час гри або іншими моментами в грі, де гравців не буде засмучувати трохи пауза).
  3. Профіль, щоб бути впевненим, що дійсно є покращення. (Досить просто; "Це працює" майже завжди б'є "не працює").

Більшу частину часу, коли я думав, що у мене може бути випадок, коли GC.Collect()варто зателефонувати, оскільки застосовані пункти 1 і 2, пункт 3 припускає, що це погіршило або принаймні зробило не кращі речі (і з незначним або без покращення я б схиляйтесь до того, щоб не викликати дзвінки, оскільки підхід швидше виявиться кращим за час роботи програми).


0

У мене є використання для вивезення сміття, яке дещо неортодоксально.

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

(Зверніть увагу, що те, що я тут називаю потворним, - це не сама модель утилізації об'єкта; те, що я називаю негарним, - це особлива IDisposable.Dispose( bool disposing )ідіома.)

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

Але тоді у вас IDisposable.Dispose()можуть бути як керовані, так і некеровані об'єкти для очищення, але керовані не можна очистити, коли IDisposable.Dispose()викликається з-під деструктора, тому що про них вже піклувався сміттєзбірник, тому там чи це потреба в окремому Dispose()методі, який приймає bool disposingпрапор, щоб знати, чи слід керувати як керованими, так і керованими об'єктами чи лише некерованими.

Вибачте, але це просто божевільно.

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

Тепер, строго кажучи, неможливо гарантувати, що жоден програміст ніколи не помилиться, забувши викликати IDisposable.Dispose(), але те, що ми можемо зробити, це використовувати деструктор, щоб зловити цю помилку. Насправді це дуже просто: все, що деструктор повинен зробити, це створити запис журналу, якщо він виявить, що disposedпрапор одноразового об'єкта ніколи не був встановлений true. Отже, використання деструктора не є невід’ємною частиною нашої стратегії утилізації, але це наш механізм забезпечення якості. А оскільки це лише тест в режимі налагодження, ми можемо розмістити весь наш деструктор всередині #if DEBUGблоку, тому ми ніколи не несемо жодного штрафу за знищення у виробничих умовах. ( IDisposable.Dispose( bool disposing )Ідіома прописує цеGC.SuppressFinalize() слід звертатися саме для того, щоб зменшити витрату на доопрацювання, але за допомогою мого механізму можна повністю уникнути цих витрат на виробничому середовищі.)

Що зводиться до цього, - це вічний аргумент про помилку проти м'якої помилки : IDisposable.Dispose( bool disposing )ідіома - це м'який підхід про помилку, і він являє собою спробу дозволити програмісту забути викликати Dispose(), якщо система не виходить з ладу. Підхід із жорсткою помилкою говорить про те, що програміст завжди повинен переконатися, що Dispose()його буде викликано. Штраф, який зазвичай призначається підходом жорстких помилок, у більшості випадків - це твердження про відмову, але для цього конкретного випадку ми робимо виняток і зменшуємо штраф до простого видачі запису журналу помилок.

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


Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()Насправді це не так, хоча я не думаю, що C # на це не здатний. Не піддавайте ресурс; натомість надайте DSL для опису всього, що ви будете робити з ним (в основному, монада), плюс функція, яка набуває ресурс, робить речі, звільняє його та повертає результат. Трюк полягає у використанні системи типів, щоб гарантувати, що якщо хтось контрабандає посилання на ресурс, він не може бути використаний в іншому виклику функції запуску.
Довал

2
Проблема Dispose(bool disposing)(яка не визначена в IDisposableтому, що вона використовується для очищення як керованих, так і некерованих об'єктів, у яких об'єкт є полем (або іншим чином відповідає), що вирішує неправильну проблему. некеровані об'єкти в керованому об'єкті, в яких немає інших одноразових об'єктів, щоб турбуватися, тоді всі Dispose()методи будуть або одним із таких (дозволити фіналізатору зробити те ж очищення, якщо необхідно), або лише керованими об'єктами розпоряджатись (не мати фіналізатора на всіх), і потреба bool disposingзникає.
Джон Ханна

-1 погана порада через те, як насправді працює фіналізація. Я повністю погоджуюся з вашою думкою щодо dispose(disposing)ідіоми террібада, але я кажу таке, тому що люди так часто використовують цю техніку та фіналізатори, коли вони мають лише керовані ресурси (наприклад, DbConnectionоб'єкт керується , він не підкреслений або не розміщений), і ви повинні ТОЛЬКО КОЖНЕ ВПРОВАДЖЕННЯ ФІНАЛІЗАТОР З НЕМЕНГАЛІЗОВАНОЮ, ПІНОВАЧЕНОЮ, КОМАНДОВАНОЮ, АБО БЕЗПЕЧНОЮ КОДУ . Я детально описав вище у своїй відповіді, як страшно дорогі фіналізатори, не використовуйте їх, якщо ви не маєте керованих ресурсів у своєму класі.
Джиммі Хоффа

2
Я майже хочу дати вам +1, хоча тільки тому, що ви декларуєте щось таке, що багато людей сприймають як головне важливе в dispose(dispoing)ідіомі, але правда це лише настільки поширене, тому що люди так бояться GC, що щось не пов'язане між собою, як що ( disposeповинен мати пшик, пов’язаний з ГК), їм належить просто приймати призначені ліки, навіть не досліджуючи його. Добре вам за те, що ви його оглянули, але ви пропустили найбільше ціле (це заохочує фіналізатори farrr частіше, ніж вони повинні бути)
Джиммі Хоффа

1
@JimmyHoffa дякую за ваш внесок. Я погоджуюся, що фіналізатор зазвичай повинен використовуватися лише для випуску некерованих ресурсів, але чи не погоджуєтесь ви, що для побудови DEBUG це правило не застосовується, і що для складання DEBUG ми повинні вільно використовувати фіналізатори для лову помилок? Це все, що я пропоную тут, тож я не бачу, чому ви ставитесь до цього питання. Дивіться також програмісти.stackexchange.com/ questions/288715/… для більш тривалого пояснення цього підходу на стороні яви світу.
Майк Накіс

0

Чи можете ви сказати мені, за яким сценарієм насправді є доброю чи розумною ідеєю змусити збирати сміття? Я не прошу конкретних випадків на C #, а навпаки, всі мови програмування, які мають смітник. Я знаю, що ви не можете застосовувати GC на всіх мовах, як-от Java, але припустимо, що ви можете.

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

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

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

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

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

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