Чи доречно * не * використовувати вільний () у виділеній пам'яті?


83

Я вивчаю комп’ютерну техніку і маю кілька курсів електроніки. Я чув від двох моїх професорів (з цих курсів) , що можна уникнути з допомогою free()функції (після того, як malloc(), calloc()і т.д.) , так як простору пам'яті , виділені , ймовірно , не використовуватиметься знову , щоб виділити іншу пам'ять. Тобто, наприклад, якщо ви виділите 4 байти, а потім випустите їх, у вас буде 4 байти місця, яке, ймовірно, не буде виділено знову: у вас буде дірка .

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

Отже: чи бувають обставини, при яких може бути доцільним використовувати malloc()без використання free()? А якщо ні, то як я можу пояснити це своїм професорам?


11
Вони не "помиляються" - вони мають дійсний (якщо обмежений) пункт про фрагментацію дуже малих ізольованих вільних регіонів, і, ймовірно, заявили про це дещо обережніше, ніж ви про це повідомляли.
Chris Stratton

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

17
@Marian: У мене був професор, який стверджував, що в C і C ++ необхідно звільнити виділену пам'ять у функції, визначеній у тому самому файлі .c / .cxx, як вона була виділена ... Ці люди іноді, здається, сильно страждають від гіпоксії через те, що живе занадто високо у вежі зі слонової кістки.
PlasmaHH

4
Існує досить багато неіграшкових програм, які не виділяють пам’ять, і дозволяти ОС прибирати все це на виході з процесу набагато швидше, ніж (метушливо) вести велику кількість книг, щоб ви могли зробити це самостійно.
Donal Fellows

9
Ніколи не зливайте речі, які ви чули, у свій мозок безперечно. У мене було багато вчителів, викладачів і виправників, які помилялися або застарівали. І завжди дуже точно аналізуйте те, що вони говорять. Наші люди часто досить точні і можуть говорити речі, які є правильними, але їх легко зрозуміти неправильно або з неправильним пріоритетом тому, хто почуває себе вдома лише простою мовою. Наприклад, я пам’ятаю, як у школі вчитель сказав: «Ви виконали домашнє завдання», я сказав «Ні». Поки я був правий, учитель визнав це образливим, бо я заощадив час, щоб знайти кульгаві виправдання, яких він не очікував.
Себастьян Мах,

Відповіді:


100

Просто: просто прочитайте джерело майже будь-якої напівсерйозної malloc()/free()реалізації. Під цим я маю на увазі фактичний менеджер пам'яті, який керує роботою дзвінків. Це може бути в середовищі виконання, віртуальній машині або операційній системі. Звичайно, код не однаково доступний у всіх випадках.

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

Отже, припустимо, ви зробили три розподілу та де-розподілу і отримали блоки, викладені в пам’яті в такому порядку:

+-+-+-+
|A|B|C|
+-+-+-+

Розміри окремих розподілів не мають значення. тоді ви звільняєте перший і останній, А і С:

+-+-+-+
| |B| |
+-+-+-+

коли ви нарешті звільняєте B, ви (спочатку, принаймні теоретично) закінчуєте:

+-+-+-+
| | | |
+-+-+-+

які можна дефрагментувати просто

+-+-+-+
|     |
+-+-+-+

тобто один більший вільний блок, фрагментів не залишилось.

Посилання за запитом:

  • Спробуйте прочитати код для dlmalloc . Я набагато просунутий, будучи повноцінною реалізацією якості виробництва.
  • Навіть у вбудованих додатках доступні реалізації дефрагментації. Див., Наприклад, ці примітки до heap4.cкоду у FreeRTOS .

1
Чи можете ви надати мені деякі посилання?
Нік

4
Думаю, варто згадати, що віртуальний адресний простір не є прямим представленням фізичної пам’яті. І з фрагментацією у фізичній пам’яті може боротися ОС, тоді як віртуальна пам’ять, не звільнена процесом, також фізично не звільняється.
lapk

@PetrBudnik, рідко було б, щоб віртуальна пам'ять так чи інакше переводила на фізичну пам'ять 1-1, ОС буде думати про відображення сторінок і матиме змогу міняти їх місцями і міняти їх з мінімальним клопотом
трещот-фрік

3
Дуже прискіпливий коментар: Хоча концепція добре пояснена, фактичний приклад був обраний трохи .. невдалий. Для тих, хто дивиться на вихідний код, скажімо, dlmalloc і заплутається: Блоки нижче певного розміру завжди мають рівні 2 і об'єднуються / розділяються відповідно. Отже, ми отримаємо (можливо) один 8-байтовий блок і 1 4-байтовий блок, але жодного 12-байтного блоку не буде. Це досить стандартний підхід для розподілювачів принаймні на робочих столах, хоча, можливо, вбудовані програми намагаються бути обережнішими з їх накладними витратами.
Voo

@Voo Я видалив згадування розміру блоків у прикладі, це все одно не мало значення. Краще?
розслабтесь

42

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

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


6
+1 Точно. Річ у тім, що якщо freeйого викликають достатньо разів, щоб мати вплив на продуктивність, то, мабуть, його також викликають достатньо разів, що, якщо його вимкнути, це зробить дуже великий вм’ятин у доступній пам’яті. Важко уявити ситуацію у вбудованій системі, коли продуктивність постійно страждає через, freeале де mallocїї викликають лише кінцеву кількість разів; це досить рідкісний випадок використання вбудованого пристрою, який виконує одноразову обробку даних, а потім скидає дані.
Jason C,

10

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


9

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

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


Дозволити ОС звільнити все в кінці - це не лише продуктивність - їй також потрібно набагато менше логіки, щоб працювати.
hugomg

@missingno Іншими словами, це дозволяє вам розібратися з тим, наскільки важким може бути управління пам’яттю :) Однак я вважав би це аргументом проти некерованих мов - якщо ваша причина полягає в складній логіці звільнення, а не в продуктивності, ви могли б бути кращими відмовитися від використання мови / середовища, яке піклується про вас.
Луаан,

5

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

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


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

2

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

Статичне розподіл відбувається, коли ви робите такі речі:

define MAX_SIZE 32
int array[MAX_SIZE];

У багатьох системах реального часу та вбудованих системах (тих, з якими найімовірніше стикаються ЕЕ чи СЕ), зазвичай переважно взагалі уникати динамічного розподілу пам'яті. Отже, використання malloc,new і їх видалення колеги рідко. Крім того, пам’ять у комп’ютерах за останні роки вибухнула.

Якщо у вас є 512 МБ для вас, і ви статично виділяєте 1 МБ, у вас є приблизно 511 МБ, щоб пройтись до того, як ваше програмне забезпечення вибухне (ну, не зовсім ... але йдіть зі мною сюди). Якщо припустити, що у вас є 511 МБ для зловживання, якщо ви щомісяця розробите 4 байти, не звільняючи їх, ви зможете працювати майже 73 години, перш ніж у вас закінчиться пам’ять. Враховуючи, що багато машин вимикаються один раз на день, це означає, що у вашої програми ніколи не закінчиться пам'ять!

У наведеному вище прикладі витік становить 4 байти в секунду, або 240 байт / хв. А тепер уявіть, що ви зменшуєте це співвідношення байт / хв. Чим нижче цей коефіцієнт, тим довше ваша програма може працювати без проблем. Якщо тиmalloc випадки рідкісні, це реальна можливість.

Чорт візьми, якщо ти знаєш, що збираєшся до mallocчогось один раз, і це mallocніколи більше не буде вражено, то це схоже на статичне розподіл, хоча тобі не потрібно знати розмір того, що ти виділяєш - спереду. Наприклад: Скажімо, у нас знову є 512 МБ. Нам потрібно матиmalloc 32 масиви цілих чисел. Це типові цілі числа - по 4 байти. Ми знаємо, що розміри цих масивів ніколи не перевищуватимуть 1024 цілих числа. Інших розподілів пам'яті в нашій програмі не відбувається. У нас вистачає пам’яті? 32 * 1024 * 4 = 131 072. 128 КБ - так так. У нас є багато місця. Якщо ми знаємо, що ніколи більше не виділятимемо пам’ять, ми можемо спокійноmallocці масиви, не звільняючи їх. Однак це також може означати, що вам доведеться перезапустити машину / пристрій, якщо програма виходить з ладу. Якщо ви запустите / зупините програму 4096 разів, ви виділите всі 512 МБ. Якщо у вас є зомбі-процеси, можливо, пам’ять ніколи не звільниться, навіть після аварії.

Бережіть себе від болю та страждань і споживайте цю мантру як Єдину правду: вона завждиmalloc повинна бути пов’язана з . завжди повинен мати . freenewdelete


2
У більшості вбудованих систем реального часу бомба із затримкою, яка спричиняє збій лише через 73 години, буде серйозною проблемою.
Бен Войгт

типові цілі числа ??? Ціле число - це принаймні 16-бітове число, а на невеликих мікросхемах воно зазвичай 16-бітове. Як правило, є більше пристроїв з sizeof(int)рівним 2, а не 4.
ST3

2

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

malloc () врешті-решт викличе mmap () або sbrk (), який завантажить сторінку з ОС.

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


2

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

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

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

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


1

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

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

Більшість програм поводиться погано. Вони розподіляють і звільняють пам'ять більш-менш випадково, у різних розмірах, від дуже маленьких до дуже великих, і вони зберігають велике використання виділених блоків. У цих програмах можливість злиття блоків обмежена, і з часом вони отримують пам'ять сильно фрагментованою і відносно несумісною. Якщо загальне використання пам’яті перевищує приблизно 1,5 ГБ у 32-розрядному просторі пам’яті, і є розмір (скажімо) 10 МБ або більше, врешті-решт одне з великих виділень не вдасться. Ці програми є загальними.

Інші програми звільняють мало або зовсім не пам'ятають, доки не зупиняться. Вони поступово розподіляють пам’ять під час роботи, звільняючи лише невеликі кількості, а потім зупиняються, і тоді вся пам’ять звільняється. Компілятор такий. Так само і ВМ. Наприклад, середовище виконання .NET CLR, написане на C ++, ймовірно, ніколи не звільняє жодної пам'яті. Чому це потрібно?

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

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

РЕДАГУВАТИ

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

Я повторю моє ключове повідомлення: балансування malloc та free не є достатнім рішенням для масштабного розподілу пам'яті у реальних програмах. Злипання блоків нормально, і виграти час, але це НЕ достатньо. Вам потрібні серйозні, розумні розподільники пам’яті, які, як правило, захоплюють пам’ять шматками (використовуючи malloc чи інше) і рідко звільняють. Ймовірно, це послання, яке мали на увазі професори ОП, яке він неправильно зрозумів.


1
Погані програми показують, що гідні розподільники повинні використовувати окремі пули для шматків різних розмірів. З віртуальною пам’яттю наявність великої кількості різних пулів на відстані не повинно бути основною проблемою. Якщо кожен шматок округлюється до рівня до двох, і кожен пул містить, таким чином, округлі шматки лише одного розміру, я не можу зрозуміти, як фрагментація може стати дуже поганою. У гіршому випадку програма може раптово перестати цікавитися певним діапазоном розмірів, залишаючи деякі в основному порожні пули, які залишаються невикористаними; Я не думаю, що це дуже поширена поведінка.
Марк ван Левен

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

12
Вся ця відповідь читається як серія випадкових здогадок та припущень. Чи є що-небудь для підтвердження цього?
Кріс Хейс,

4
Я не впевнений, що ви маєте на увазі, кажучи, що середовище виконання .NET CLR не звільняє жодної пам'яті. Наскільки я тестував, це робить, якщо може.
Теодорос Чатсігіананкаіс,

1
@vonbrand: GCC має кілька розподільників, включаючи власну марку збирача сміття. Він споживає пам'ять під час проходів і збирає її між проходами. Більшість компіляторів для інших мов має щонайбільше 2 проходи та вільну пам’ять між проходами або майже не містить. Якщо ви не згодні, я із задоволенням досліджую будь-який ваш приклад.
david.pfx

1

Я здивований, що Книгу ще ніхто не цитував :

Зрештою це може бути неправдою, оскільки пам’яті можуть стати достатньо великими, щоб неможливо було закінчити вільну пам’ять протягом життя комп’ютера. Наприклад, щорічно відбувається приблизно 3 10 10 13 мікросекунд, тож, якщо ми маємо один раз за мікросекунду, нам знадобиться приблизно 10 15 комірок пам’яті, щоб побудувати машину, яка могла б працювати 30 років, не втрачаючи пам’яті. Стільки пам’яті здається абсурдно великим за сучасними мірками, але фізично це неможливо. З іншого боку, процесори стають все швидшими, і майбутній комп’ютер може мати велику кількість процесорів, що працюють паралельно на одній пам’яті, тож можливо використовувати пам’ять набагато швидше, ніж ми вважали.

http://sarabander.github.io/sicp/html/5_002e3.xhtml#FOOT298

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


1

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

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