Haskell требует сборщика мусора?

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

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

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

8 ответов


как уже указывали другие, 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] должны храниться в память до тех пор, пока пользователь не наберет "clear"; поэтому время жизни этого должны определяется динамически, и именно поэтому необходимо динамическое управление памятью.

таким образом, в этом смысле необходимо автоматическое динамическое выделение памяти, и на практике это означает:да, Haskell требует сборщика мусора, так как сборщик мусора является высокопроизводительным автоматическим динамическим менеджером памяти.

однако...

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

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

мы можем надеяться, что компилятор обнаружит это x2 можно безопасно освободить когда f возвращает (вместо того, чтобы ждать, пока сборщик мусора освободит x2). По сути, мы просим компилятор выполнить Escape-анализ преобразовать выделение в мусорная куча до распределения в стеке везде, где это возможно.

это не слишком неразумно просить:МХК Хаскеле компилятор делает это, хотя GHC этого не делает. Саймон Марлоу!--48-->говорит что сборщик мусора поколения 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'ы области.

за Хаскелл!--36-->

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

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

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

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


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

f (x, y)

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

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

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


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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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


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

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

EDIT: я забыл. Лень-это АД без GC. Не веришь мне? Просто попробуйте без GC в, например, C++. Вот увидишь ... вещи!--1-->