Чому компілятори автоматично не вставляють переговори?


63

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

EDIT: Для подальшого ознайомлення, ось ще одна дискусія, яка має цікавий приклад.


125
І це, діти, саме тому ми навчаємо вас теорії обчислюваності. ;)
Рафаель

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

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

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

2
@BorisTreukhov, будь ласка, віднесіть його до бесіди. Ні, я не думаю, що Андрій каже, що аналіз втечі "неможливий" (хоча точно визначити, що це означає в цьому контексті, для мене трохи незрозуміло). Абсолютно точний аналіз втечі є нерозв'язним. Для всіх: будь ласка, віднесіть його до кімнати чату . Будь ласка, публікуйте тут лише коментарі, які спрямовані на покращення питання - інші дискусії та коментарі повинні розміщуватися в чаті.
DW

Відповіді:


80

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


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

1
Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
Жиль

2
Хлопці, візьміть до чату . Все, що безпосередньо не стосується самої відповіді та як її можна вдосконалити, буде видалено.
Рафаель

2
Багато речей, які компілятори щасливо роблять, взагалі не можна визначити; ми б не потрапили ніде в світі компілятора, якби ми завжди відповідали теоремі Райса.
Тихон Єлвіс

3
Це не має значення. Якщо це неможливо визначити для всіх компіляторів, це також не можна визначити для всіх людей. Але ми очікуємо, що люди вставлять free()правильно.
Пол Дрейпер

53

Як справедливо зазначив Девід Річербі, проблема в цілому не вирішена. Життєвість об'єктів є загальною властивістю програми і може взагалі залежати від вкладень у програму.

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

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

Реалізації, засновані на підрахунку посилань, дуже близькі до "компілятора, що вставляє угоди", таким чином, що важко визначити різницю. LLVM «и автоматичний підрахунок посилань (використовується в Objective-C і Swift ) є відомим прикладом.

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

Зараз, що стосується людини, існує три основні способи управління життями розподілу вручну:

  1. Розуміючи програму та проблему. Люди, наприклад, можуть розміщувати предмети, що мають подібний час життя, в один і той же об'єкт розподілу. Компілятори та збирачі сміття повинні робити це, але люди мають більш точну інформацію.
  2. Шляхом вибіркового використання нелокальної бухгалтерії (наприклад, підрахунок посилань) або інших спеціальних методів розподілу (наприклад, зон) лише за потреби. Знову ж таки, людина може знати це там, де компілятор повинен зробити це висновком.
  3. Погано. Всі знають про реально розгорнуті програми, які повільно протікають. Або якщо цього не відбувається, іноді програми та внутрішні API потрібно реструктурувати протягом життя, зменшуючи повторне використання та модульність.

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

2
Це, безумовно, найкраща відповідь на питання (яке занадто багато відповідей навіть не стосується). Ви могли б додати посилання на новаторську роботу Ганса Бема про загальнодоступний GC : en.wikipedia.org/wiki/Boehm_garbage_collector . Ще один цікавий момент полягає в тому, що життєздатність даних (або корисність у розширеному розумінні) можна визначити стосовно абстрактної семантики або моделі виконання. Але тема справді широка.
бабу

29

Це проблема незавершеності, а не проблема нерозбірливості

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

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

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

Але якщо вихідний код C не передбачає явної команди для розстановки, то компілятору необхідно зробити висновок, що він повинен виконати розділення, коли користувач введе відповідну команду в REPL. Це команда "deallocate", "free" чи щось інше? У компілятора немає способу знати, якою ви хочете бути командою. Навіть якщо ви програмуєте в логіці шукати це командне слово і REPL знаходить його, у компілятора немає способу знати, що він повинен відповідати на нього дислокацією, якщо ви прямо не скажете це у вихідному коді.

tl; dr Проблема в тому, що вихідний код C не надає компілятору зовнішніх знань. Невизначеність не є проблемою, оскільки там є, чи є процес ручним чи автоматизованим.


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

23

Наразі жодна з опублікованих відповідей не є повністю коректною.

Чому компілятори автоматично не вставляють переговори?

  1. Деякі так і роблять. (Я поясню пізніше.)

  2. Тривіально, ви можете зателефонувати free()безпосередньо перед виходом програми. Але у вашому питанні є неявна потреба зателефонувати free()якнайшвидше.

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

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

  5. Люди (намагаються) записують у підмножину програм C, які можна перевірити на правильність пам'яті за своїм алгоритмом (самі).

  6. Деякі мови виконують №1, будуючи №5 у компіляторі. Вони не дозволяють програмам з довільним використанням розподілу пам’яті, а скоріше визначальну підмножину їх. Foth і Rust - це два приклади мов, які мають більш обмежувальний розподіл пам'яті, ніж C malloc(), які можуть (1) визначати, чи програма написана поза їх вирішальним набором (2) автоматично вставляти міркування.


1
Я розумію, як це робить Іржа. Але я ніколи не чув про Forth, який це робив. Чи можете ви докладно?
Мілтон Сільва

2
@MiltonSilva, Forth - принаймні його найосновніша, оригінальна реалізація - має лише стек, а не купу. Це робить розподіл / делокацію переміщенням покажчика стека викликів, завдання, яке компілятор може легко виконати. Форт був зроблений для націлювання на дуже просте обладнання, а іноді нединамічна пам'ять - це все, що є працездатним. Це, очевидно, не є ефективним рішенням для нетривіальних програм.
Пол Дрейпер

10

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

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


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

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

"Ми можемо якийсь день, можливо, не надто далеко, бути заміненим штучним інтелектом на роботі". Це, зокрема, нісенітниця. Люди - джерело намірів у системі. Без людей немає жодної мети для системи. "Штучний інтелект" можна визначити як прозорість інтелектуального рішення сучасного часу машинами, спричинене фактично розумними рішеннями програміста або дизайнера системи в минулому. Якщо технічного обслуговування (яке повинно бути виконано людиною) немає, AI (або будь-яка система, яка залишилася несподіваною і повністю автоматичною), вийде з ладу.
Wildcard

Намір, як у людини, так і в машинах, завжди надходить ззовні .
Андре Суза Лемос

Цілком неправдиві. (А також "зовні" не визначає джерело. ) Або ви заявляєте, що такий намір насправді не існує, або ви заявляєте, що намір існує, але не походить нізвідки. Можливо, ви вважаєте, що намір може існувати незалежно від мети? У цьому випадку ви неправильно розумієте слово "умисел". Так чи інакше, особиста демонстрація незабаром змінить вашу думку з цього приводу. Я відмовчуся від цього коментаря, оскільки слова самі по собі не можуть породити розуміння "наміру", тому подальше обговорення тут безглуздо.
Wildcard

9

Відсутність автоматичного управління пам’яттю - особливість мови.

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


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
DW

2
Як це відповідь на (CS частина питання)?
Рафаель

6
@Raphael Інформатика не означає, що нам слід шукати незрозумілі технічні відповіді. Компілятори роблять багато речей, які неможливі в загальному випадку. Якщо ми хочемо автоматичне управління пам’яттю, ми можемо реалізувати його багатьма способами. С не робить цього, бо цього не слід робити.
Jouni Sirén

9

Питання - це переважно історичний артефакт, а не неможливість реалізації.

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

Якщо ви подивитесь на більш сучасні мови низького рівня, такі як C ++ 11 або Rust, ви побачите, що вони здебільшого вирішили проблему, зробивши явним право власності на пам'ять у типі вказівника. У C ++ ви б використовували unique_ptr<T>замість простої T*для утримання пам’яті, і unique_ptr<T>гарантуєте, що пам'ять звільняється, коли об’єкт досягає кінця області, на відміну від звичайної T*. Програміст може передати пам'ять від однієї unique_ptr<T>до іншої, але коли-небудь може бути лише один unique_ptr<T>вказівник на пам'ять. Тож завжди зрозуміло, кому належить пам’ять і коли її потрібно звільнити.

З міркувань зворотної сумісності C ++, як і раніше, дозволяє керувати пам'яттю в старому стилі і, таким чином, створювати помилки або способи обійти захист unique_ptr<T>. Іржа ще більш сувора в тому, що вона застосовує правила власності на пам'ять за допомогою помилок компілятора.

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


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
Рафаель

6

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

Одне питання, яке досі не висвітлено, - це неминуча затримка вивезення сміття. У C, коли програміст дзвонить безкоштовно (), ця пам'ять одразу доступна для повторного використання. (Принаймні теоретично!) Так програміст може звільнити свою структуру 100 МБ, виділити ще одну структуру 100 Мб на мілісекунди пізніше і очікувати, що загальне використання пам'яті залишиться таким же.

Це не вірно з вивезенням сміття. Системи, що збираються сміттям, мають певну затримку з поверненням невикористаної пам’яті до купи, і це може бути суттєво. Якщо ваша структура 100 МБ виходить за межі сфери, а через мільйон секунд ваша програма створить ще одну структуру 100 МБ, ви можете з розумом очікувати, що ваша система буде використовувати 200 МБ протягом короткого періоду. Цей "короткий період" може становити мілісекунди або секунди залежно від системи, але затримка все ж є.

Якщо ви працюєте на ПК з гігами оперативної пам’яті та віртуальної пам’яті, звичайно, ви, мабуть, цього ніколи не помітите. Якщо ви працюєте в системі з обмеженими ресурсами (скажімо, вбудованою системою або телефоном), це щось, що потрібно серйозно поставитися до цього. Це не просто теоретично - я особисто бачив, що це створює проблеми (як, наприклад, у збої типу проблем на пристрої) під час роботи над системою WinCE за допомогою .NET Compact Framework та розробці в C #.


Теоретично ви могли запускати GC перед кожним розподілом.
adrianN

4
@adrianN Але на практиці це не робиться, оскільки це буде розумово. Справа Грема досі стоїть: GC завжди мають значні витрати, як з точки зору виконання, так і з точки зору необхідної надлишкової пам'яті. Ви можете налаштувати цей баланс на будь-який крайній край, але принципово не можете зняти накладні витрати.
Конрад Рудольф

"Затримка", коли пам'ять звільняється, є більшою проблемою в системі віртуальної пам'яті, ніж у системі з обмеженими ресурсами. У першому випадку програмі може бути краще використовувати 100 МБ, ніж 200 МБ, навіть якщо в системі є 200 МБ , але в останньому випадку не буде користі для запуску ГК раніше, ніж потрібно, якщо затримки не будуть прийнятнішими під час деяких частини коду, ніж під час інших.
supercat

Я не бачу, як це намагається відповісти на (CS частина питання).
Рафаель

1
@Raphael Я пояснив добре визнану проблему з принципом вивезення сміття, який (або слід навчати) в CS як один з його основних недоліків. Я навіть дав свій особистий досвід, коли я це бачив на практиці, щоб показати, що це не чисто теоретична проблема. Якщо ви щось не зрозуміли з цього приводу, я радий поговорити з вами, щоб покращити свої знання з цього питання.
Грем

4

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

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

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


4
"" На даний момент у програмі посилання на пам'ять FOO вже не корисне "- це інформація, відома лише розуму програміста" - це явно неправильно. a) Для багатьох FOO це неважливо розібратися, наприклад, локальні змінні зі значеннями семантики. б) Ви припускаєте, що програміст знає це завжди, що, очевидно, є надто оптимістичним припущенням; якби це правда, у нас не було б серйозних помилок через погану обробку пам'яті, це критично важливе для безпеки програмне забезпечення. Що, на жаль, ми робимо.
Рафаель

Я просто припустити , що мова був розроблений для випадків , коли програміст дійсно знає FOO не корисно більше. Я погоджуюся, очевидно, що це зазвичай не відповідає дійсності, і саме тому нам потрібно провести статичний аналіз та / або збирання сміття. Що, ура, ми робимо. Але питання ОП полягає в тому, коли ці речі не такі цінні, як ручні кодовані оператори?
Тревіс Вілсон

4

Що буде зроблено: Існує сміття, і є компілятори з допомогою підрахунку посилань (Objective-C, Swift). Ті, хто робить підрахунок посилань, потребують допомоги програміста, уникаючи сильних еталонних циклів.

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

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


Я не бачу тут внеску.
бабу

3

На таких мовах, як C, програміст повинен вставляти дзвінки безкоштовно. Чому компілятор не робить цього автоматично?

Тому що термін служби блоку пам'яті - це рішення програміста, а не компілятора.

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

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


-1

На таких мовах, як C, програміст повинен вставляти дзвінки безкоштовно. Чому компілятор не робить цього автоматично?

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

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

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

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