Чи потрібен Haskell збирач сміття?


118

Мені цікаво, чому в реалізаціях Haskell використовується GC.

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

Я шукаю приклад коду, який би протікав, якщо GC не було.


14
Можливо, ця серія виявиться освічуючою; Він висвітлює те, як створюється (і згодом збирається сміття): blog.ezyang.com/2011/04/the-haskell-heap
Том Крокетт

5
всюди є посилання на чисті мови! просто не змінні посилання.
Том Крокетт

1
@pelotom Посилання на незмінні дані або незмінні посилання?
Паббі

3
І те й інше. Те, що зазначені дані є незмінними, випливає з того, що всі посилання незмінні, аж донизу.
Том Крокетт

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

Відповіді:


218

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

Наприклад, розглянемо таку програму:

main = loop (Just [1..1000]) where
  loop :: Maybe [Int] -> IO ()
  loop obj = do
    print obj
    resp <- getLine
    if resp == "clear"
     then loop Nothing
     else loop obj

У цій програмі список [1..1000]повинен зберігатися в пам'яті до тих пір, поки користувач не набере "очистити"; тому час життя цього повинен визначатися динамічно, і саме тому необхідне динамічне управління пам’яттю.

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

Однак ...

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

f :: Integer -> Integer
f x = let x2 = x*x in x2*x2

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

Це не надто необґрунтовано просити: компілятор jhc haskell робить це, хоча GHC - ні. Саймон Марлоу каже, що генераційний сміттєзбірник GHC робить аналіз втечі здебільшого непотрібним.

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

f :: Integer -> (Integer, Integer)
f x = let x2 = x * x in (x2, x2+1)

g :: Integer -> Integer
g x = case f x of (y, z) -> y + z

У цьому випадку спрощений аналіз втечі може зробити висновок, що x2втеча з f(тому що він повертається в кортежі), а значить, x2повинен бути розподілений на купі зібраного сміття. З іншого боку, умовивід регіону здатний виявити, що x2може бути розміщено при gповерненні; ідея тут полягає в тому, що x2слід виділяти в gрегіоні Росії, а не fв регіоні.

Поза Haskell

Хоча висновок про область корисний у певних випадках, як обговорювалося вище, здається, що складно ефективно примиритись з ледачою оцінкою (див. Коментарі Едварда Кметта та Саймона Пейтона Джонса ). Наприклад, розглянемо

f :: Integer -> Integer
f n = product [1..n]

Можна спокуситись виділити список [1..n]на стек і розподілити його після fповернення, але це було б катастрофічно: він змінився б fіз використання O (1) пам'яті (під збиранням сміття) на O (n) пам'яті.

У 1990-х та на початку 2000-х років велика робота була проведена над висновком регіону для суворої функціональної мови ML. Мадс Тофте, Ларс Біркедал, Мартін Елсман, Нільс Галленберг написали досить читаючу ретроспективу про свою роботу щодо виводу регіону, значну частину якої вони інтегрували в компілятор MLKit . Вони експериментували з чисто регіональним управлінням пам’яттю (тобто не збирачем сміття), а також гібридним управлінням пам’яті, що базується на регіонах / зібраним сміттям, і повідомили, що їхні програми тестування працювали «в 10 разів швидше і в 4 рази повільніше», ніж чисті сміття, зібрані версії.


2
Чи потрібен Haskell спільний доступ? Якщо ні, у першому прикладі ви можете передати копію списку (респ. Nothing) На рекурсивний виклик loopта перемістити старий - невідомо життя. Звичайно, ніхто не хоче реалізовувати Haskell, що не ділиться спільно, тому що це страшенно повільно для великих структур даних.
німі

3
Мені дуже подобається ця відповідь, хоча моя єдина плутанина - з першим прикладом. Очевидно, що якщо користувач ніколи не вводив "clear", то він може використовувати нескінченну пам'ять (без GC), але це не зовсім тече, оскільки пам'ять все ще відслідковується.
Паббі

3
C ++ 11 має чудову реалізацію розумних покажчиків. В основному тут використовується підрахунок посилань. Я думаю, що Хаскелл міг викинути сміття на користь чогось подібного, а тому стати детермінованим.
intrepidis

3
@ChrisNash - не працює. Розумні покажчики використовують підрахунок посилань під кришкою. Підрахунок посилань не може мати справу з структурами даних з циклами. Haskell може генерувати структури даних за допомогою циклів.
Стівен С

3
Я не впевнений, чи згоден я з частиною цієї відповіді щодо динамічного розподілу пам'яті. Тільки тому, що програма не знає, коли користувач тимчасово перестане циклічно, не повинна робити це динамічним. Це визначається тим, чи знає компілятор, чи вийде щось із контексту. У випадку Хаскелла, де це формально визначено самою граматикою мови, життєвий контекст відомий. Однак пам'ять може все-таки бути динамічною з тієї причини, що вирази та тип списку динамічно генеруються всередині мови.
Тимофій Лебідь

27

Візьмемо тривіальний приклад. Враховуючи це

f (x, y)

(x, y)перед тим, як дзвонити, потрібно десь виділити пару f. Коли ви можете розібрати цю пару? Ви поняття не маєте. Він не може бути розподілений при fповерненні, тому що, fможливо, він включив пару в структуру даних (наприклад, f p = [p]), тому час життя пари, можливо, повинен бути довшим, ніж повернення з f. Тепер скажіть, що пара була внесена до списку, чи може хто, хто розділяє цей список, розділити пару? Ні, тому що пара може бути спільною (наприклад, let p = (x, y) in (f p, p)). Тож насправді важко сказати, коли пару можна розселити.

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

Тож я хотів би перевернути це питання. Як ви вважаєте, чому Haskell не потрібен GC. Як би ви запропонували зробити розподіл пам'яті?


18

Ваша інтуїція, що це має відношення до чистоти, має певну правду.

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

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

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

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

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

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

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

Ще один (більш абстрактний) спосіб мислення щодо цього - зазначити, що Хаскелл побудований з просто набраного лямбдального числення, яке базується на теорії декартових закритих категорій і що такі категорії оснащені діагональною функцією diag :: X -> (X, X). Мова, заснована на іншому класі категорії, може не мати такого.

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


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

14

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

Ось чому програми GHC, як правило, мають такі високі показники загального розподілу (від гігабайт до терабайт): вони постійно виділяють пам'ять, і лише завдяки ефективному GC вони відновлюють її до закінчення.


2
"вони ніколи не змінюють попередні значення": ви можете перевірити haskell.org/haskellwiki/HaskellImplementorsWorkshop/2011/Takano , мова йде про експериментальне розширення GHC, яке повторно використовує пам'ять.
gfour

11

Якщо мова (будь-яка мова) дозволяє динамічно розподіляти об'єкти, то є три практичних способи поводження з керуванням пам'яттю:

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

  2. Мова може надати явний freeабо disposeмеханізм. Але це покладається на програміста, щоб правильно це зробити. Будь-яка помилка в управлінні сховищем може призвести до витоку пам'яті ... або ще гірше.

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

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

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

Я не можу придумати випадок, коли GC був би необхідний чистою мовою.

Імовірно, ви маєте на увазі чисту функціональну мову.

Відповідь полягає в тому, що під кришкою потрібен GC, щоб відтворити купу об'єктів, які ОБОВ'ЯЗКОВО створити. Наприклад.

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

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

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

Я шукаю приклад коду, який би протікав, якщо GC не було.

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


2
Чому ви вважаєте, що ваш список варіантів є вичерпним? ARC в Цілі C, висновок регіону в MLKit та DDC, збирання сміття під час збирання в Меркурії - всі вони не входять до цього списку.
Ді пн

@DeeMon - всі вони входять в одну з цих категорій. Якщо ви думаєте, що це не так, тому що ви занадто щільно малюєте межі категорії. Коли я кажу "якась форма вивезення сміття", я маю на увазі будь-який механізм, в якому зберігання відновлюється автоматично.
Стівен C

1
C ++ 11 використовує смарт-покажчики. В основному тут використовується підрахунок посилань. Це детерміновано і автоматично. Я хотів би бачити, як реалізація Haskell використовує цей метод.
intrepidis

2
@ChrisNash - 1) Це не працюватиме. Рекультивація опорних лічильників витокує дані, якщо існують цикли ... якщо ви не можете покластися на код програми для розбиття циклів. 2) Загальновідомо (людям, які вивчають ці речі), що підрахунок посилань працює погано в порівнянні з сучасним (справжнім) сміттєзбірником.
Стівен С

@DeeMon - окрім того, дивіться відповідь Рейнерпа про те, чому висновок регіону не був би практичним з Haskell.
Стівен С

8

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


Це дає відповідь, чому GC взагалі непотрібний, але мене більше цікавить Haskell.
Паббі

10
Якщо GC теоретично непотрібний взагалі, то виходить, що теоретично непотрібний і для Haskell.
ehird

@ehird Я мав намір сказати необхідне , я вважаю, що моя перевірка орфографії перелічила значення.
Паббі

1
Коментар Ehird досі тримається :-)
Paul R

2

GC "must have" у чистих мовах FP. Чому? Операції alloc і free є нечистими! А друга причина полягає в тому, що незмінна рекурсивна структура даних потребує GC для існування, оскільки зворотне посилання створює непрості та нездійсненні структури людського розуму. Звичайно, зворотній зв’язок - це благо, тому що копіювання структур, які використовують його, коштує дуже дешево.

У будь-якому випадку, якщо ви мені не вірите, просто спробуйте впровадити мову FP, і ви побачите, що я маю рацію.

EDIT: Я забув. Лінь - ПЕКЛО без ГК. Не вірите мені? Просто спробуйте без GC, наприклад, у C ++. Ви побачите ... речі


1

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

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


1
У деяких випадках статичний аналіз може вставляти в ті грошові коди, які звільняють деякі дані після того, як обчислюється потік. Розміщення відбудеться під час виконання, але це не GC. Це схоже на ідею підрахунку посилань на інтелектуальні покажчики в C ++. Розум про тривалість життя об'єкта відбувається під час виконання, але GC не використовується.
Ді пн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.