Яким чином відбувається вивезення сміття мовами, які складаються на місцях?


79

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

Я розумію, як збирання сміття могло працювати з інтерпретованою мовою. Збірник сміття просто працює поруч з інтерпретатором і видаляє невикористані та недоступні об'єкти із пам'яті програми. Вони обоє бігають разом.

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

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

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

Однією з таких мов програмування є Ейфель. Більшість компіляторів Ейфеля генерують код C з міркувань портативності. Цей код C використовується для створення машинного коду стандартним компілятором C. Реалізації Ейфеля забезпечують GC (а іноді навіть точний GC) для цього складеного коду, і в VM немає необхідності. Зокрема, компілятор VisualEiffel генерував власний машинний код x86 безпосередньо з повною підтримкою GC .

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

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

D - мова програмування систем з підтримкою збору сміття. Зазвичай не потрібно явно звільняти пам’ять. Просто виділіть за потребою, і сміттєзбірник періодично повертає всю невикористану пам'ять у пул доступної пам’яті.

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

Або я хибний у своєму мисленні? Якщо так, то які методи використовуються для здійснення збору сміття для складених мов і як саме вони працюватимуть?


1
Буду вдячний, якби близький виборець цього питання міг би вказати, що саме не так, щоб я міг це виправити?
Крістіан Дін

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

7
Ви можете вважати, що збирач сміття є частиною бібліотеки часу виконання, яка реалізує мову, еквівалентну мові malloc().
Бармар

9
Робота сміттєзбірника залежить від особливостей розподільника , а не від моделі складання . Розподільник знає кожен виділений об'єкт; вона їх виділяла. Тепер все, що вам потрібно, - це певний спосіб дізнатися, які об’єкти ще живі , і колектор може розмістити всі об'єкти, крім них. Ніщо в цьому описі не має нічого спільного з моделлю компіляції.
Ерік Ліпперт

1
GC - це особливість динамічної пам'яті, а не особливість інтерпретатора.
Дмитро Григор’єв

Відповіді:


52

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

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

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

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

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


9
Між бібліотеками утиліт та компіляцією JIT рядки між "компільованим до рідного" та "виконуваним у середовищі виконання" стають все більш розмитими.
corsiKa

6
Просто додати трохи про мови, які не підтримуються GC: Це правда, що C та інші подібні мови не надають інформацію про стеки викликів, але якщо ви все в порядку з певним кодом платформи (як правило, включаючи трохи коду складання) все ще можливо впровадити "консервативне вивезення сміття". Приклад Boehm GC є прикладом цього, що використовується в реальних програмах життя.
Матті Вірккунен

2
@corsiKa А точніше, лінія набагато виразніша. Тепер ми бачимо, що це різні непов'язані поняття, а не антоніми інших.
Кролтан

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

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

123

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

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


51
@ChristianDean Зауважте, що навіть у C є бібліотека часу виконання. Хоча він не має GC, він все ще виконує управління пам'яттю через цю бібліотеку виконання: malloc()і free()не вбудований в мову, не є частиною операційної системи, але є функціями цієї бібліотеки. C ++ також іноді компілюється з бібліотекою збору сміття, навіть якщо мова не була розроблена з урахуванням GC.
амон

18
C ++ також містить бібліотеку часу виконання, яка виконує такі дії, як створення dynamic_castта винятки, навіть якщо ви не додаєте GC.
Себастьян Редл

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

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

15
@millimoose: Так. Наприклад, у GCC цей фрагмент коду є crt0.o(Що означає " C R un T ime, самі основи"), який зв'язаний з кожною програмою (або принаймні кожною програмою, яка не є вільною ).
Йорг W Міттаг

58

Або компілятор включає якийсь мінімальний збирач сміття в код складеної програми.

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

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

Але, можливо, GC відрізняється від цих інших бібліотек, які надають явні API, які викликає користувач?

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

  1. Поширення винятків та виклик стеки / виклик деструктора.
  2. Динамічне розподіл пам’яті (що зазвичай не є лише викликом функції, як у C, навіть коли немає сміття).
  3. Відстеження інформації про динамічний тип (для ролей тощо).

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


це , здається, не пропонує нічого істотного по точках з і роз'яснено в верхньому відповіді відправив 3 години до
комара

11
@gnat Я вважав це корисним / необхідним, оскільки відповідь на сьогодні недостатньо сильна: він згадує подібні факти, але не вказує, що виділення сміття - це абсолютно штучне відмінність. Принципово, припущення ОП є хибним, і головна відповідь про це не згадує. Шахта робить (уникаючи досить чіткого терміну "хибний").
Конрад Рудольф

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

7
@millimoose Бібліотеки виконання функціонують за кадром безлічі способів без явного взаємодії з користувачем. Розглянемо розповсюдження винятків та виклик стеки / виклик деструктора. Розглянемо динамічний розподіл пам’яті (що зазвичай не є лише викликом функції, як у C, навіть коли немає сміття). Розглянемо обробку інформації про динамічний тип (для лиття тощо). Тож GC насправді не унікальний.
Конрад Рудольф

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

23

Як би це працювало зі складеними мовами?

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

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

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

Зауважте, що інтерпретатори та компілятори мають слабке значення, і деякі мовні реалізації можна вважати обома. Іншими словами, між ними існує континуум. Прочитайте останню Книгу Драконів і подумайте про байт-код , компіляцію JIT , що динамічно випромінює код C, який збирається в якийсь "плагін", а потім dlopen (3) -ed тим самим процесом (і на сучасних машинах, це досить швидко, щоб бути сумісним з інтерактивний реплєї см це )


Я настійно рекомендую ознайомитися з посібником з GC . Для відповіді потрібна ціла книга . Перед цим прочитайте вікі-сторінку " Збір сміття" (яку, мабуть, ви прочитали перед читанням нижче).

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

То як же складена програма може збирати сміття?

Просто випромінюючи машинний код, який використовує (і "дружній" і "сумісний з") систему виконання.

Зауважте, що ви можете знайти декілька бібліотек збору сміття, зокрема Boehm GC , MPS Ravenbrook або навіть мій ( непідтримуваний ) Qish . А кодувати простий GC не дуже складно (проте, налагодити його важче, а кодувати конкурентоспроможний GC важко ).

У деяких випадках компілятор використовував консервативний GC (наприклад, Boehm GC ). Тоді, кодувати нема чого. Консервативний GC (коли компілятор викликає процедуру розподілу або всю програму GC) іноді сканує весь стек викликів і припускає, що будь-яка зона пам’яті (опосередковано), доступна зі стека викликів, є реальною. Це називається консервативним GC, оскільки втрачається інформація про введення тексту: якщо ціле число в стеку виклику стане схожим на якусь адресу, воно буде дотримуватися і т.д.

В інших (більш складних) випадках час виконання покоління збирає сміття з покоління (типовим прикладом є компілятор Ocaml, який компілює код Ocaml для машинного коду за допомогою такого ГК). Тоді питання полягає в тому, щоб точно знайти на стеках виклику всі вказівники, а деякі з них переміщуються GC. Потім компілятор генерує метадані, що описують кадри стека викликів, які використовуються під час виконання. Таким чином, умовні вимоги та ABI стають специфічними для цієї системи (тобто компілятора) та системи виконання.

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

Чи зберігає компілятор копію якоїсь програми збору сміття та вставляє її у кожен створений нею виконаний файл?

Різновид. Однак система виконання може бути спільною бібліотекою тощо. Іноді (на Linux та декількох інших системах POSIX) вона навіть може бути інтерпретатором сценарію, наприклад, передається в execve (2) з шебангом . Або перекладач ELF , див. Ельф (5) та PT_INTERPін.

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


5
Ви маєте на увазі, що існує безліч реалізацій мови програмування без явної специфікації. Так, я згоден з цим. Але моя думка полягає в тому, що мова програмування - це не програмне забезпечення (наприклад, якийсь компілятор чи якийсь перекладач). Це щось, що має синтаксис та семантику (можливо, обидва є неправильно визначеними).
Базиль Старинкевич

4
@KonradRudolph: Це повністю залежить від вашого визначення поняття "формальна" та "специфікація" :-D Існує специфікація мови програмування ISO / IEC 30170: 2012 Ruby , яка визначає невеликий підмножина перетину Ruby 1.8 та 1.9. Існує пакет Ruby Spec Suite - набір прикладів прикордонних справ, які служать своєрідною "виконаною специфікацією". Потім «Мова програмування Рубі» Девіда Фланагана та Юкіхіро Мацумото .
Йорг W Міттаг

4
Також документація Ruby . Обговорення питань на трекуванні Ruby Issue . Дискусії про розсилки ruby-core (англійська) та ruby-dev (японська). Очікування здорового глузду від громади (наприклад Array#[], O (1) найгірший, Hash#[]це O (1) амортизований найгірший випадок). І останнє, але не менш важливе: мозок маца.
Йорг W Міттаг

6
@KonradRudolph: Справа в тому, що навіть мова, яка не має офіційних специфікацій і лише одна вкладення, все ще може бути розділена на "мову" (абстрактні правила та обмеження) та "реалізацію" (програми обробки коду відповідно до цих правил і обмеження). І реалізація все-таки породжує специфікацію, хоч і банальну, а саме: "все, що робить код, є специфікацією". Ось так написано специфікацію ISO, RubySpec та RDocs: врешті-решт, граючи навколо дотепної та / або зворотної інженерної МРТ.
Йорг W Міттаг

1
Радий, що ви виховували сміттєзбірника Богема. Я б рекомендував вивчити цю програму, оскільки це чудовий приклад того, яким простим вивезенням сміття може бути навіть тоді, коли "зафіксовано" на існуючому компіляторі.
Корт Аммон

6

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

Сам по собі не існує такого поняття, як "рідно складена мова". Наприклад, той самий код Java був інтерпретований (а потім частково вчасно складений під час виконання) на моєму старому телефоні (Java Dalvik) і (достроково) компілюється на моєму новому телефоні (ART).

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

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

Виділяється деяка вільна пам'ять, конструктор викликається і т. Д. Коли не вистачає пам'яті, тоді викликається сміттєзбірник. Оскільки ви вже перебуваєте в режимі виконання, який є рідним кодом, існування перекладача зовсім не має значення.

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

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


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


І Ocaml, і SBCL є власними компіляторами. Отже, є реалізовані "нативно складені мови".
Базиле Старинкевич

@BasileStarynkevitch WAT? Як називання деяких менш відомих компіляторів стосується моєї відповіді? Хіба SBCL не є аргументом на користь мого твердження, що розрізнення не має сенсу?
maaartinus

Звичайний Lisp (або будь-яка інша мова) не інтерпретується та не складається. Це мова програмування (специфікація). Його реалізація може бути компілятором, або інтерпретатором, або чимось між ними (наприклад, інтерпретатор байт-коду). SBCL - це інтерактивна компільована реалізація Common Lisp. Ocaml також є мовою програмування (як інтерпретатор байт-коду, так і власний компілятор).
Василь Старинкевич

@BasileStarynkevitch Ось що я стверджую. 1. Немає такого поняття, як інтерпретована або складена мова (хоча C трактується рідко, а LISP використовується рідко, але це насправді не має значення). 2. Існують інтерпретовані, компільовані та змішані реалізації для більшості відомих мов, і немає жодної мови, що не перешкоджає компіляції чи інтерпретації.
maaartinus

6
Я думаю, що ваш аргумент має багато сенсу. Ключовим моментом для grok є те, що ви завжди запускаєте "рідну програму" або "ніколи", проте ви хочете її бачити. Жодна програма exe в Windows сама по собі не виконується; йому потрібен завантажувач та інші функції ОС лише для запуску, і насправді частково "інтерпретується" також. Це стає більш очевидним із виконуваними файлами .net. java myprog- це стільки ж, як мало рідного, grep myname /etc/passwdабо ld.so myprog: Це виконавчий файл (що б це не означало), який бере аргумент і виконує операції з даними.
Пітер А. Шнайдер

3

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

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

У поступовому та одночасному GC складений код та GC повинні співпрацювати для підтримки деяких інваріантів. Наприклад, у колекторі копіювання GC працює, копіюючи живі дані з простору A в простір B, залишаючи позаду сміття. Для наступного циклу він перевертає A і B і повторюється. Таким чином, одним із правил може бути забезпечення того, що в будь-який час програма користувача намагається звернутися до об'єкта в просторі A, це виявляється, і об'єкт негайно копіюється в простір B, де програма може продовжувати доступ до нього. Адреса переадресації залишається в просторі A, щоб вказати GC, що це сталося, щоб будь-які інші посилання на об'єкт оновлювалися по мірі їх відстеження. Це відомо як "бар'єр читання".

Алгоритми ГК вивчаються ще з 60-х років, і існує велика література з цього питання. Google, якщо ви хочете отримати більше інформації.

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