Haskell에 가비지 수집기가 필요합니까?


118

Haskell 구현이 GC를 사용하는 이유가 궁금합니다.

순수한 언어로 GC가 필요한 경우는 생각할 수 없습니다. 복사를 줄이기위한 최적화일까요, 아니면 실제로 필요한가요?

GC가 없으면 누출되는 예제 코드를 찾고 있습니다.


14
이 시리즈는 깨달음을 얻을 수 있습니다. 그것은 쓰레기가 생성 (및 이후 수집)하는 방법을 설명합니다 : blog.ezyang.com/2011/04/the-haskell-heap
톰 크로켓

5
순수한 언어로 된 참조가 어디에나 있습니다! 가변 참조가 아닙니다 .
Tom Crockett

1
@pelotom 불변 데이터 또는 불변 참조에 대한 참조?
Pubby 2012 년

3
양자 모두. 참조 된 데이터가 변경 불가능하다는 사실은 모든 참조가 변경 불가능하다는 사실에서 비롯됩니다.
Tom Crockett

4
이러한 추론을 메모리 할당에 적용 하면 일반적인 경우에 할당 해제를 정적으로 예측할 수없는 이유를 이해하는 데 도움이 되므로 중단 문제 에 확실히 관심이있을 것 입니다. 그러나 거기에 일부 해제는 그들이처럼 예측 될 수있는 프로그램 몇 가지 사실을 실행하지 않고 종료 알 수 프로그램.
Paul R

답변:


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]은 사용자가 "clear"를 입력 할 때까지 메모리에 보관되어야합니다. 따라서 이것의 수명은 동적으로 결정 되어야 하며, 이것이 동적 메모리 관리가 필요한 이유입니다.

그래서 이런 의미에서, 자동화 된 동적 메모리 할당이 필요하며, 실제로이 방법은 : 가비지 컬렉션은 최고 성능의 자동 동적 메모리 관리자이기 때문에, 하스켈은 가비지 컬렉터가 필요합니다.

하나...

가비지 수집기가 필요하지만 컴파일러가 가비지 수집보다 저렴한 메모리 관리 체계를 사용할 수있는 특수한 경우를 찾으려고 할 수 있습니다. 예를 들어, 주어진

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

컴파일러가 반환 x2시 안전하게 할당 해제 될 수 있음 을 감지하기를 바랍니다 f(가비지 수집기의 할당 해제를 기다리는 대신 x2). 본질적으로 컴파일러는 가능한 경우 할당을 가비지 수집 된 힙으로 변환 하여 스택의 할당으로 변환 하도록 이스케이프 분석 을 수행하도록 요청 합니다.

이것은 요청하기에 너무 불합리 하지 않습니다. GHC는 그렇지 않지만 jhc haskell 컴파일러 가이를 수행합니다. Simon Marlow 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할당되어야한다는 것 입니다.gf

하스켈을 넘어서

지역 추론은 위에서 논의한 특정 경우에 도움이되지만 지연 평가와 효과적으로 조화를 이루는 것은 어렵습니다 ( Edward KmettSimon Peyton Jones의 의견 참조). 예를 들어

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

[1..n]스택에 목록을 할당하고 f반환 후에 할당을 해제하려는 유혹 이있을 수 있지만 이것은 치명적일 것입니다. fO (1) 메모리 (가비지 수집 아래)를 사용하는 것에서 O (n) 메모리로 변경됩니다.

엄격한 기능적 언어 ML에 대한 지역 추론에 대한 광범위한 작업이 1990 년대와 2000 년대 초에 수행되었습니다 . Mads Tofte, Lars Birkedal, Martin Elsman, Niels Hallenberg는 지역 추론에 대한 작업에 대해 꽤 읽기 쉬운 회고전 을 작성했으며 , 대부분은 MLKit 컴파일러에 통합되었습니다 . 그들은 순수 지역 기반 메모리 관리 (예 : 가비지 수집기 없음)와 하이브리드 영역 기반 / 가비지 수집 메모리 관리를 실험했으며 테스트 프로그램이 순수 가비지보다 "10 배 더 빠르게 4 배 더 느리게"실행되었다고보고했습니다. 수집 된 버전.


2
Haskell은 공유가 필요합니까? 그렇지 않은 경우 첫 번째 예에서 목록 사본 (각각 Nothing)을 재귀 호출에 전달 loop하고 알 수없는 수명이 아닌 이전 호출을 할당 해제 할 수 있습니다. 물론 대규모 데이터 구조의 경우 끔찍하게 느리기 때문에 누구도 Haskell의 비공유 구현을 원하지 않습니다.
nimi

3
첫 번째 예에 대한 유일한 혼란이 있지만 나는이 대답을 정말 좋아합니다. 분명히 사용자가 "clear"를 입력하지 않으면 무한 메모리 (GC없이)를 사용할 수 있지만 메모리가 여전히 추적되고 있으므로 정확히 누수는 아닙니다.
Pubby 2012 년

3
C ++ 11에는 스마트 포인터가 훌륭하게 구현되어 있습니다. 기본적으로 참조 계수를 사용합니다. Haskell은 비슷한 것을 선호하여 가비지 컬렉션을 버리고 결정론 적이 될 수 있다고 생각합니다.
intrepidis 2013 년

3
@ChrisNash-작동하지 않습니다. 스마트 포인터는 내부적으로 참조 계산을 사용합니다. 참조 카운팅은주기가있는 데이터 구조를 처리 할 수 ​​없습니다. Haskell은 주기로 데이터 구조를 생성 할 수 있습니다.
Stephen C

3
이 답변의 동적 메모리 할당 부분에 동의하는지 잘 모르겠습니다. 프로그램이 사용자가 시간적으로 반복을 중지 할 때를 알지 못하기 때문에 동적으로 만들지 않아야합니다. 이는 컴파일러가 컨텍스트에서 벗어날 수 있는지 여부에 따라 결정됩니다. 언어 문법 자체에 의해 공식적으로 정의되는 Haskell의 경우 삶의 맥락이 알려져 있습니다. 그러나 목록 표현식과 유형이 언어 내에서 동적으로 생성되기 때문에 메모리는 여전히 동적 일 수 있습니다.
Timothy Swan

27

간단한 예를 들어 보겠습니다. 이것을 감안할 때

f (x, y)

(x, y)호출하기 전에 어딘가에 쌍을 할당해야합니다 f. 언제 그 쌍을 할당 해제 할 수 있습니까? 당신은 몰라요. 쌍을 데이터 구조 (예 :)에 넣었을 수 있으므로 f반환 시 할당을 취소 할 수 없으므로 ff p = [p]의 수명이에서 반환하는 것보다 길어야 할 수 있습니다 f. 이제 쌍이 목록에 포함되었다고 가정하면 목록을 분리하는 사람이 쌍을 할당 해제 할 수 있습니까? 아니요, 쌍이 공유 될 수 있기 때문입니다 (예 :) let p = (x, y) in (f p, p). 따라서 쌍이 언제 할당 해제 될 수 있는지 알기가 정말 어렵습니다.

Haskell의 거의 모든 할당에 대해 동일하게 적용됩니다. 즉, 수명에 상한선을 제공하는 분석 (지역 분석)이 가능합니다. 이것은 엄격한 언어에서 합리적으로 잘 작동하지만 게으른 언어에서는 덜 작동합니다 (게으른 언어는 구현에서 엄격한 언어보다 훨씬 많은 변형을 수행하는 경향이 있습니다).

그래서 저는 질문을 바꾸고 싶습니다. Haskell이 GC를 필요로하지 않는 이유는 무엇입니까? 메모리 할당을 어떻게 제안 하시겠습니까?


18

이것이 순수함과 관련이 있다는 당신의 직감은 그것에 대한 진실을 가지고 있습니다.

Haskell은 함수의 부작용이 유형 시그니처에서 설명되기 때문에 부분적으로 순수하게 간주됩니다. 따라서 함수에 무언가를 인쇄하는 부작용이있는 경우 IO반환 유형에 어딘가 가 있어야합니다 .

그러나 Haskell의 모든 곳에서 암묵적으로 사용되는 함수가 있으며 어떤 의미에서 부작용을 설명하지 않는 유형 서명이 있습니다. 즉, 일부 데이터를 복사하고 두 가지 버전을 다시 제공하는 기능입니다. 내부적으로는 메모리에 데이터를 복제하여 문자 그대로 작동하거나 나중에 상환해야하는 부채를 늘려 '가상'으로 작동 할 수 있습니다.

복사 기능을 허용하지 않는 훨씬 더 제한적인 유형 시스템 (순전히 "선형"시스템)으로 언어를 디자인 할 수 있습니다. 그런 언어를 쓰는 프로그래머의 관점에서 하스켈은 약간 불순 해 보인다.

사실, Haskell의 친척 인 Clean 은 선형 (더 엄격하게 : 고유 한) 유형을 가지고 있으며 복사를 허용하지 않는 것이 어떤 것인지 알 수 있습니다. 그러나 Clean은 여전히 ​​"고유하지 않은"유형에 대한 복사를 허용합니다.

분야 에 대한 많은 연구가 있으며 Google에서 충분히 검색하면 가비지 수집이 필요없는 순수한 선형 코드의 예를 찾을 수 있습니다. 컴파일러가 GC의 일부를 제거 할 수 있도록 어떤 메모리가 사용될 수 있는지 컴파일러에 신호를 보낼 수있는 모든 종류의 유형 시스템을 찾을 수 있습니다.

양자 알고리즘도 순전히 선형이라는 감각이 있습니다. 모든 작업은 되돌릴 수 있으므로 데이터를 생성, 복사 또는 삭제할 수 없습니다 . (일반적인 수학적 의미에서도 선형입니다.)

중복이 발생할 때 명확하게하는 명시적인 DUP 작업이있는 Forth (또는 다른 스택 기반 언어)와 비교하는 것도 흥미 롭습니다.

이것에 대한 또 다른 (더 추상적 인) 사고 방식은 Haskell이 데카르트 폐쇄 범주 이론을 기반으로하는 단순 유형의 람다 미적분으로 구성되어 있으며 이러한 범주에는 대각선 함수가 장착되어 있다는 점에 주목하는 것 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에서 동적 데이터 구조를 에뮬레이션 할 수 있습니다. 끔찍합니다 ...이 논의와 관련이 없습니다.)

  2. 언어는 명시 적 free또는 dispose메커니즘을 제공 할 수 있습니다 . 그러나 이것은 올바른 것을 얻기 위해 프로그래머에게 달려 있습니다. 스토리지 관리의 실수로 인해 메모리 누수가 발생할 수 있습니다.

  3. 언어 (또는 더 엄격하게는 언어 구현)는 동적으로 할당 된 스토리지를위한 자동 스토리지 관리자를 제공 할 수 있습니다. 즉, 어떤 형태의 가비지 수집기.

유일한 다른 옵션은 동적으로 할당 된 스토리지를 절대 재 확보하지 않는 것입니다. 이것은 작은 계산을 수행하는 작은 프로그램을 제외하고는 실용적인 해결책이 아닙니다.

이것을 Haskell에 적용하면 언어는 1의 제한이 없으며 2에 따른 수동 할당 해제 작업이 없습니다. 따라서 사소한 일에 사용할 수 있으려면 Haskell 구현에 가비지 수집기가 포함되어야합니다. .

순수한 언어로 GC가 필요한 경우는 생각할 수 없습니다.

아마도 당신은 순수한 기능적 언어를 의미합니다.

대답은 언어가 반드시 만들어야하는 힙 객체를 회수하기 위해 내부적으로 GC가 필요하다는 것입니다. 예를 들면.

  • 순수한 함수는 경우에 따라 힙 객체를 반환해야하기 때문에 힙 객체를 만들어야합니다. 이는 스택에 할당 할 수 없음을 의미합니다.

  • 순환이있을 수 있다는 사실은 ( let rec예를 들어) 참조 계산 방식이 힙 개체에 대해 작동하지 않음을 의미합니다.

  • 그런 다음 함수 클로저가 있습니다 ... 또한 스택에 할당 할 수 없습니다. 이는 생성 된 스택 프레임과 (일반적으로) 독립적 인 수명을 갖기 때문입니다.

GC가 없으면 누출되는 예제 코드를 찾고 있습니다.

클로저 또는 그래프 형태의 데이터 구조를 포함하는 거의 모든 예는 이러한 조건에서 누출됩니다.


2
옵션 목록이 완전하다고 생각하는 이유는 무엇입니까? Objective C의 ARC, MLKit 및 DDC의 지역 추론, Mercury의 컴파일 타임 가비지 수집-모두이 목록에 맞지 않습니다.
Dee Mon

@DeeMon-모두 해당 범주 중 하나에 적합합니다. 그렇지 않다고 생각한다면 카테고리 경계를 너무 빡빡하게 그리기 때문입니다. "일부 형태의 가비지 콜렉션"이라고하면 스토리지가 자동으로 회수되는 메커니즘 을 의미 합니다 .
Stephen C

1
C ++ 11은 스마트 포인터를 사용합니다. 기본적으로 참조 계수를 사용합니다. 결정적이며 자동입니다. 이 방법을 사용하는 Haskell 구현을보고 싶습니다.
intrepidis

2
@ChrisNash-1) 작동하지 않습니다. 주기를 끊기 위해 애플리케이션 코드에 의존 할 수없는 경우 참조 카운트 기반 교정은주기가있는 경우 데이터를 누출합니다. 2) 현대 (실제) 가비지 수집기와 비교할 때 참조 계산이 제대로 수행되지 않는다는 것은 잘 알려져 있습니다 (이러한 것들을 연구하는 사람들에게).
스티븐 C

@DeeMon-게다가 지역 추론이 Haskell에서 실용적이지 않은 이유에 대한 Reinerp의 답변을 참조하십시오.
Stephen C

8

충분한 메모리가 있으면 가비지 수집기는 필요하지 않습니다. 그러나 실제로 우리는 무한한 메모리를 가지고 있지 않으므로 더 이상 필요하지 않은 메모리를 회수 할 수있는 방법이 필요합니다. C와 같은 불순한 언어에서는 메모리를 해제하기 위해 메모리를 모두 사용했음을 명시 적으로 나타낼 수 있습니다.하지만 이것은 변경 작업입니다 (방금 해제 한 메모리는 더 이상 읽기에 안전하지 않습니다). 따라서이 접근 방식을 사용할 수 없습니다. 순수한 언어. 따라서 메모리를 해제 할 수있는 위치를 정적으로 분석하거나 (일반적인 경우 불가능할 수 있음) 체처럼 메모리를 누수하거나 (모두 사용할 때까지 잘 작동 함) GC를 사용합니다.


이것은 GC가 일반적으로 불필요한 이유에 대한 대답이지만 특히 Haskell에 더 관심이 있습니다.
Pubby

10
GC가 이론적으로 일반적으로 불필요하다면 이론적으로도 Haskell에 대해서도 불필요하다는 사실을 알 수 있습니다.
ehird

@ehird 내가 필요하다고 말하려고했는데 , 내 맞춤법 검사기가 의미를 뒤집 었다고 생각합니다.
Pubby

1
세 번째 의견은 여전히 ​​유지됩니다 :-)
Paul R

2

GC는 순수한 FP 언어에서 "필수"입니다. 왜? 할당 및 자유 작업은 불순합니다! 두 번째 이유는 변경 불가능한 재귀 데이터 구조가 존재하기 위해 GC가 필요하다는 것입니다. 백 링크는 인간의 마음을 위해 난해하고 유지 불가능한 구조를 생성하기 때문입니다. 물론 backlinking은 축복입니다. 왜냐하면 그것을 사용하는 구조를 복사하는 것은 매우 저렴하기 때문입니다.

어쨌든, 당신이 나를 믿지 않는다면, FP 언어를 구현하려고 노력하면 내가 옳다는 것을 알게 될 것입니다.

편집 : 나는 잊었다. 게으름은 GC가없는 지옥입니다. 나를 믿지 않습니까? 예를 들어 C ++에서 GC없이 시도해보십시오. 당신은 볼 것입니다 ...


1

Haskell은 엄격하지 않은 프로그래밍 언어이지만 대부분의 구현에서는 필요에 따라 호출 (게으름)을 사용하여 비 엄격 성을 구현합니다. call-by-need에서는 "thunks"(평가되기를 기다린 다음 자신을 덮어 쓰는 표현식, 필요할 때 값을 재사용 할 수 있도록 표시됨)를 사용하여 런타임 중에 도달 할 때만 항목을 평가합니다.

따라서 썽크를 사용하여 언어를 느리게 구현하면 런타임 인 ​​마지막 순간까지 객체 수명에 대한 모든 추론을 연기 한 것입니다. 이제 평생에 대해 아무것도 모르기 때문에 합리적으로 할 수있는 것은 가비지 수집뿐입니다.


1
어떤 경우에는 정적 분석이 썽크가 평가 된 후 일부 데이터를 해제하는 썽크 코드에 삽입 될 수 있습니다. 할당 해제는 런타임에 발생하지만 GC는 아닙니다. 이것은 C ++에서 스마트 포인터를 계산하는 참조 개념과 유사합니다. 객체 수명에 대한 추론은 런타임에서 발생하지만 GC는 사용되지 않습니다.
Dee Mon
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.