Требуется ли Haskell сборщик мусора? - PullRequest
112 голосов
/ 31 марта 2012

Мне любопытно, почему реализации на Haskell используют GC.

Я не могу вспомнить случай, когда GC был бы необходим на чистом языке.Это просто оптимизация, чтобы уменьшить копирование, или это действительно необходимо?

Я ищу пример кода, который утек бы, если бы GC не было.

Ответы [ 8 ]

213 голосов
/ 31 марта 2012

Как уже отмечали другие, 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). По сути, мы просим, ​​чтобы компилятор выполнил escape-анализ для преобразования выделений в кучу для сбора мусора в выделений в стеке , где это возможно.

Не так уж и необоснованно просить: компилятор 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.

За Хаскелем

Хотя вывод по региону полезен в некоторых случаях, как обсуждалось выше, представляется, что трудно эффективно примирить с ленивой оценкой (см. и 1010 * * * * * комментариев Саймона Пейтона Джонса Эдварда Кметта). Например, рассмотрим

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

Кто-то может испытать желание разместить список [1..n] в стеке и освободить его после возврата f, но это будет катастрофическим: это изменит f при использовании памяти O (1) (при сборке мусора) в O (n) памяти.

В 1990-х и начале 2000-х годов была проделана большая работа по выводу региона для функционального языка строгого ML. Мадс Тофт, Ларс Биркедал, Мартин Эльсман, Нильс Халленберг написали вполне читабельную ретроспективу о своей работе по определению региона, большую часть которой они интегрировали в MLKit-компилятор . Они экспериментировали с управлением памятью исключительно на основе регионов (т.е. без сборщика мусора), а также с гибридным управлением памятью на основе регионов / мусора, и сообщили, что их тестовые программы работали «в 10 раз быстрее и в 4 раза медленнее», чем чистый мусор. собранные версии.

26 голосов
/ 31 марта 2012

Давайте рассмотрим тривиальный пример. Учитывая это

f (x, y)

вам нужно выделить пару (x, y) где-то перед вызовом f. Когда вы можете освободить эту пару? У тебя нет идей. Он не может быть освобожден, когда возвращается f, поскольку f мог бы поместить пару в структуру данных (например, f p = [p]), поэтому время жизни пары могло бы быть длиннее, чем возвращение из f. Теперь, скажем, что пара была помещена в список, может ли кто-нибудь разобрать список, освободить пару? Нет, потому что пара может быть разделена (например, let p = (x, y) in (f p, p)). Так что действительно сложно сказать, когда можно освободить пару.

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

Так что я хотел бы перевернуть вопрос. Как вы думаете, почему Haskell не нужен GC. Как бы вы предложили выделить память?

16 голосов
/ 01 апреля 2012

Ваша интуиция, что это как-то связано с чистотой, имеет некоторую правду.

Haskell считается чистым частично потому, что побочные эффекты функций учитываются в сигнатуре типа.Поэтому, если у функции есть побочный эффект печати чего-либо, где-то в ее типе возврата должен быть IO.

Но есть функция, которая неявно используется везде в Haskell, и чья сигнатура типа необъяснить, что в некотором смысле является побочным эффектом.А именно функция, которая копирует некоторые данные и возвращает вам две версии.Под капотом это может работать либо буквально, путем дублирования данных в памяти, либо «виртуально» путем увеличения долга, который необходимо погасить позже.

Можно проектировать языки с еще более ограниченными системами типов (чисто «линейные», которые запрещают функцию копирования.С точки зрения программиста на таком языке, Haskell выглядит немного нечистым.

На самом деле Clean , родственник Haskell, имеет линейные (точнее: уникальные) типыи это может дать некоторое представление о том, на что было бы похоже запретить копирование.Но Clean по-прежнему позволяет копировать для «неуникальных» типов.

В этой области есть много исследований , и если вам достаточно Google, вы найдете примеры чистого линейного кода, который не требуетвывоз мусора.Вы найдете все виды систем типов, которые могут сигнализировать компилятору, какую память можно использовать, что позволяет компилятору исключить некоторые из GC.

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

Интересно также сравнить с Forth (или другими языками на основе стека), которые имеют явные операции DUP, которые ясно дают понять, когда происходит дублирование.

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

Но в целом, чисто линейное программирование слишком сложно, чтобы быть полезным, поэтому мы соглашаемся на GC.

14 голосов
/ 31 марта 2012

Стандартные методы реализации, применяемые к Haskell, на самом деле требуют GC больше, чем большинство других языков, поскольку они никогда не изменяют предыдущие значения, вместо этого создают новые, измененные значения на основе предыдущих. Поскольку это означает, что программа постоянно выделяет и использует больше памяти, большое количество значений будет отброшено с течением времени.

Вот почему программы GHC обычно имеют такие высокие общие показатели распределения (от гигабайтов до терабайтов): они постоянно выделяют память, и только благодаря эффективному сборщику мусора они освобождают ее перед тем, как заканчиваются.

10 голосов
/ 31 марта 2012

Если язык (любой язык) позволяет динамически распределять объекты, есть три практических способа управления памятью:

  1. Язык позволяет вам выделять память только в стеке или при запуске. Но эти ограничения строго ограничивают виды вычислений, которые может выполнять программа. (На практике. Теоретически вы можете эмулировать динамические структуры данных в (скажем) Фортране, представляя их в большом массиве. Это УЖАСНО ... и не имеет отношения к этому обсуждению.)

  2. Язык может предоставлять явный механизм free или dispose. Но это зависит от программиста, чтобы сделать это правильно. Любая ошибка в управлении хранилищем может привести к утечке памяти ... или хуже.

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

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

Применяя это к Haskell, язык не имеет ограничения 1. и нет операции ручного освобождения согласно пункту 2. Следовательно, для того, чтобы его можно было использовать для нетривиальных вещей, реализация Haskell должна включать сборщик мусора.

Я не могу вспомнить случай, когда GC был бы необходим на чистом языке.

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

Ответ заключается в том, что GC требуется под капотом для восстановления объектов кучи, которые язык ДОЛЖЕН создать. Например.

  • Чистая функция должна создавать объекты кучи, потому что в некоторых случаях она должна возвращать их. Это означает, что они не могут быть размещены в стеке.

  • Тот факт, что могут быть циклы (например, let rec), означает, что метод подсчета ссылок не будет работать для объектов кучи.

  • Затем существуют замыкания функций ... которые также не могут быть распределены в стеке, поскольку их время жизни (как правило) не зависит от того кадра стека, в котором они были созданы.

Я ищу пример кода, который утек бы, если бы GC не было.

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

8 голосов
/ 31 марта 2012

Сборщик мусора не требуется, если у вас достаточно памяти. Однако в действительности у нас нет бесконечной памяти, и поэтому нам нужен какой-то метод для восстановления памяти, которая больше не нужна. В нечистых языках, таких как C, вы можете явно заявить, что вы сделали с некоторой памятью, чтобы освободить ее - но это операция с мутированием (только что освобожденная память больше не безопасна для чтения), поэтому вы не можете использовать этот подход в чистый язык. Так что либо статически проанализируйте, где вы можете освободить память (возможно, в общем случае это невозможно), утечка памяти, как сито (прекрасно работает, пока не закончится), либо использовать GC.

1 голос
/ 31 марта 2012

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

Итак, если вы лениво реализуете свой язык, используя thunks, вы откладываете все рассуждения о времени жизни объекта до последнего момента, который является временем выполнения. Поскольку теперь вы ничего не знаете о жизнях, единственное, что вы можете разумно сделать, это сборщик мусора ...

0 голосов
/ 15 сентября 2015

GC "должен иметь" на чистых языках FP.Зачем?Операции распределенные и бесплатные нечисты!И вторая причина в том, что неизменяемые рекурсивные структуры данных нуждаются в GC для существования, потому что обратные ссылки создают заумные и не поддерживаемые структуры для человеческого разума.Конечно, обратные ссылки - это благословение, потому что копирование структур, которые их используют, очень дешево.

В любом случае, если вы мне не верите, просто попробуйте реализовать язык FP, и вы увидите, что я прав.

РЕДАКТИРОВАТЬ: я забыл.Лень это АД без ГК.Не веришь мне?Просто попробуйте без GC, например, в C ++.Вы увидите ... вещи

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...