Haskell 구현이 GC를 사용하는 이유가 궁금합니다.
순수한 언어로 GC가 필요한 경우는 생각할 수 없습니다. 복사를 줄이기위한 최적화일까요, 아니면 실제로 필요한가요?
GC가 없으면 누출되는 예제 코드를 찾고 있습니다.
Haskell 구현이 GC를 사용하는 이유가 궁금합니다.
순수한 언어로 GC가 필요한 경우는 생각할 수 없습니다. 복사를 줄이기위한 최적화일까요, 아니면 실제로 필요한가요?
GC가 없으면 누출되는 예제 코드를 찾고 있습니다.
답변:
다른 사람들이 이미 지적했듯이 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
할당되어야한다는 것 입니다.g
f
지역 추론은 위에서 논의한 특정 경우에 도움이되지만 지연 평가와 효과적으로 조화를 이루는 것은 어렵습니다 ( Edward Kmett 및 Simon Peyton Jones의 의견 참조). 예를 들어
f :: Integer -> Integer
f n = product [1..n]
[1..n]
스택에 목록을 할당하고 f
반환 후에 할당을 해제하려는 유혹 이있을 수 있지만 이것은 치명적일 것입니다. f
O (1) 메모리 (가비지 수집 아래)를 사용하는 것에서 O (n) 메모리로 변경됩니다.
엄격한 기능적 언어 ML에 대한 지역 추론에 대한 광범위한 작업이 1990 년대와 2000 년대 초에 수행되었습니다 . Mads Tofte, Lars Birkedal, Martin Elsman, Niels Hallenberg는 지역 추론에 대한 작업에 대해 꽤 읽기 쉬운 회고전 을 작성했으며 , 대부분은 MLKit 컴파일러에 통합되었습니다 . 그들은 순수 지역 기반 메모리 관리 (예 : 가비지 수집기 없음)와 하이브리드 영역 기반 / 가비지 수집 메모리 관리를 실험했으며 테스트 프로그램이 순수 가비지보다 "10 배 더 빠르게 4 배 더 느리게"실행되었다고보고했습니다. 수집 된 버전.
Nothing
)을 재귀 호출에 전달 loop
하고 알 수없는 수명이 아닌 이전 호출을 할당 해제 할 수 있습니다. 물론 대규모 데이터 구조의 경우 끔찍하게 느리기 때문에 누구도 Haskell의 비공유 구현을 원하지 않습니다.
간단한 예를 들어 보겠습니다. 이것을 감안할 때
f (x, y)
(x, y)
호출하기 전에 어딘가에 쌍을 할당해야합니다 f
. 언제 그 쌍을 할당 해제 할 수 있습니까? 당신은 몰라요. 쌍을 데이터 구조 (예 :)에 넣었을 수 있으므로 f
반환 시 할당을 취소 할 수 없으므로 f
쌍 f p = [p]
의 수명이에서 반환하는 것보다 길어야 할 수 있습니다 f
. 이제 쌍이 목록에 포함되었다고 가정하면 목록을 분리하는 사람이 쌍을 할당 해제 할 수 있습니까? 아니요, 쌍이 공유 될 수 있기 때문입니다 (예 :) let p = (x, y) in (f p, p)
. 따라서 쌍이 언제 할당 해제 될 수 있는지 알기가 정말 어렵습니다.
Haskell의 거의 모든 할당에 대해 동일하게 적용됩니다. 즉, 수명에 상한선을 제공하는 분석 (지역 분석)이 가능합니다. 이것은 엄격한 언어에서 합리적으로 잘 작동하지만 게으른 언어에서는 덜 작동합니다 (게으른 언어는 구현에서 엄격한 언어보다 훨씬 많은 변형을 수행하는 경향이 있습니다).
그래서 저는 질문을 바꾸고 싶습니다. Haskell이 GC를 필요로하지 않는 이유는 무엇입니까? 메모리 할당을 어떻게 제안 하시겠습니까?
이것이 순수함과 관련이 있다는 당신의 직감은 그것에 대한 진실을 가지고 있습니다.
Haskell은 함수의 부작용이 유형 시그니처에서 설명되기 때문에 부분적으로 순수하게 간주됩니다. 따라서 함수에 무언가를 인쇄하는 부작용이있는 경우 IO
반환 유형에 어딘가 가 있어야합니다 .
그러나 Haskell의 모든 곳에서 암묵적으로 사용되는 함수가 있으며 어떤 의미에서 부작용을 설명하지 않는 유형 서명이 있습니다. 즉, 일부 데이터를 복사하고 두 가지 버전을 다시 제공하는 기능입니다. 내부적으로는 메모리에 데이터를 복제하여 문자 그대로 작동하거나 나중에 상환해야하는 부채를 늘려 '가상'으로 작동 할 수 있습니다.
복사 기능을 허용하지 않는 훨씬 더 제한적인 유형 시스템 (순전히 "선형"시스템)으로 언어를 디자인 할 수 있습니다. 그런 언어를 쓰는 프로그래머의 관점에서 하스켈은 약간 불순 해 보인다.
사실, Haskell의 친척 인 Clean 은 선형 (더 엄격하게 : 고유 한) 유형을 가지고 있으며 복사를 허용하지 않는 것이 어떤 것인지 알 수 있습니다. 그러나 Clean은 여전히 "고유하지 않은"유형에 대한 복사를 허용합니다.
이 분야 에 대한 많은 연구가 있으며 Google에서 충분히 검색하면 가비지 수집이 필요없는 순수한 선형 코드의 예를 찾을 수 있습니다. 컴파일러가 GC의 일부를 제거 할 수 있도록 어떤 메모리가 사용될 수 있는지 컴파일러에 신호를 보낼 수있는 모든 종류의 유형 시스템을 찾을 수 있습니다.
양자 알고리즘도 순전히 선형이라는 감각이 있습니다. 모든 작업은 되돌릴 수 있으므로 데이터를 생성, 복사 또는 삭제할 수 없습니다 . (일반적인 수학적 의미에서도 선형입니다.)
중복이 발생할 때 명확하게하는 명시적인 DUP 작업이있는 Forth (또는 다른 스택 기반 언어)와 비교하는 것도 흥미 롭습니다.
이것에 대한 또 다른 (더 추상적 인) 사고 방식은 Haskell이 데카르트 폐쇄 범주 이론을 기반으로하는 단순 유형의 람다 미적분으로 구성되어 있으며 이러한 범주에는 대각선 함수가 장착되어 있다는 점에 주목하는 것 diag :: X -> (X, X)
입니다. 다른 범주의 범주에 기반한 언어에는 그런 것이 없을 수도 있습니다.
그러나 일반적으로 순전히 선형 프로그래밍은 유용하기가 너무 어렵 기 때문에 GC에 만족합니다.
Haskell에 적용된 표준 구현 기술은 이전 값을 변경하지 않고 대신 이전 값을 기반으로 새로 수정 된 값을 생성하기 때문에 실제로 대부분의 다른 언어보다 GC가 더 많이 필요합니다. 이것은 프로그램이 지속적으로 더 많은 메모리를 할당하고 사용한다는 것을 의미하므로 시간이 지남에 따라 많은 수의 값이 삭제됩니다.
이것이 GHC 프로그램이 기가 바이트에서 테라 바이트까지 총 할당 수치가 높은 경향이있는 이유입니다. 지속적으로 메모리를 할당하고 있으며, 고갈되기 전에이를 회수하는 것은 효율적인 GC 덕분입니다.
언어 (모든 언어)를 통해 객체를 동적으로 할당 할 수있는 경우 메모리 관리를 처리하는 세 가지 실용적인 방법이 있습니다.
이 언어는 스택 또는 시작시에만 메모리를 할당하도록 허용 할 수 있습니다. 그러나 이러한 제한은 프로그램이 수행 할 수있는 계산의 종류를 심각하게 제한합니다. (실제로는 이론적으로는 큰 배열로 표현하여 Fortran에서 동적 데이터 구조를 에뮬레이션 할 수 있습니다. 끔찍합니다 ...이 논의와 관련이 없습니다.)
언어는 명시 적 free
또는 dispose
메커니즘을 제공 할 수 있습니다 . 그러나 이것은 올바른 것을 얻기 위해 프로그래머에게 달려 있습니다. 스토리지 관리의 실수로 인해 메모리 누수가 발생할 수 있습니다.
언어 (또는 더 엄격하게는 언어 구현)는 동적으로 할당 된 스토리지를위한 자동 스토리지 관리자를 제공 할 수 있습니다. 즉, 어떤 형태의 가비지 수집기.
유일한 다른 옵션은 동적으로 할당 된 스토리지를 절대 재 확보하지 않는 것입니다. 이것은 작은 계산을 수행하는 작은 프로그램을 제외하고는 실용적인 해결책이 아닙니다.
이것을 Haskell에 적용하면 언어는 1의 제한이 없으며 2에 따른 수동 할당 해제 작업이 없습니다. 따라서 사소한 일에 사용할 수 있으려면 Haskell 구현에 가비지 수집기가 포함되어야합니다. .
순수한 언어로 GC가 필요한 경우는 생각할 수 없습니다.
아마도 당신은 순수한 기능적 언어를 의미합니다.
대답은 언어가 반드시 만들어야하는 힙 객체를 회수하기 위해 내부적으로 GC가 필요하다는 것입니다. 예를 들면.
순수한 함수는 경우에 따라 힙 객체를 반환해야하기 때문에 힙 객체를 만들어야합니다. 이는 스택에 할당 할 수 없음을 의미합니다.
순환이있을 수 있다는 사실은 ( let rec
예를 들어) 참조 계산 방식이 힙 개체에 대해 작동하지 않음을 의미합니다.
그런 다음 함수 클로저가 있습니다 ... 또한 스택에 할당 할 수 없습니다. 이는 생성 된 스택 프레임과 (일반적으로) 독립적 인 수명을 갖기 때문입니다.
GC가 없으면 누출되는 예제 코드를 찾고 있습니다.
클로저 또는 그래프 형태의 데이터 구조를 포함하는 거의 모든 예는 이러한 조건에서 누출됩니다.
충분한 메모리가 있으면 가비지 수집기는 필요하지 않습니다. 그러나 실제로 우리는 무한한 메모리를 가지고 있지 않으므로 더 이상 필요하지 않은 메모리를 회수 할 수있는 방법이 필요합니다. C와 같은 불순한 언어에서는 메모리를 해제하기 위해 메모리를 모두 사용했음을 명시 적으로 나타낼 수 있습니다.하지만 이것은 변경 작업입니다 (방금 해제 한 메모리는 더 이상 읽기에 안전하지 않습니다). 따라서이 접근 방식을 사용할 수 없습니다. 순수한 언어. 따라서 메모리를 해제 할 수있는 위치를 정적으로 분석하거나 (일반적인 경우 불가능할 수 있음) 체처럼 메모리를 누수하거나 (모두 사용할 때까지 잘 작동 함) GC를 사용합니다.
GC는 순수한 FP 언어에서 "필수"입니다. 왜? 할당 및 자유 작업은 불순합니다! 두 번째 이유는 변경 불가능한 재귀 데이터 구조가 존재하기 위해 GC가 필요하다는 것입니다. 백 링크는 인간의 마음을 위해 난해하고 유지 불가능한 구조를 생성하기 때문입니다. 물론 backlinking은 축복입니다. 왜냐하면 그것을 사용하는 구조를 복사하는 것은 매우 저렴하기 때문입니다.
어쨌든, 당신이 나를 믿지 않는다면, FP 언어를 구현하려고 노력하면 내가 옳다는 것을 알게 될 것입니다.
편집 : 나는 잊었다. 게으름은 GC가없는 지옥입니다. 나를 믿지 않습니까? 예를 들어 C ++에서 GC없이 시도해보십시오. 당신은 볼 것입니다 ...
Haskell은 엄격하지 않은 프로그래밍 언어이지만 대부분의 구현에서는 필요에 따라 호출 (게으름)을 사용하여 비 엄격 성을 구현합니다. call-by-need에서는 "thunks"(평가되기를 기다린 다음 자신을 덮어 쓰는 표현식, 필요할 때 값을 재사용 할 수 있도록 표시됨)를 사용하여 런타임 중에 도달 할 때만 항목을 평가합니다.
따라서 썽크를 사용하여 언어를 느리게 구현하면 런타임 인 마지막 순간까지 객체 수명에 대한 모든 추론을 연기 한 것입니다. 이제 평생에 대해 아무것도 모르기 때문에 합리적으로 할 수있는 것은 가비지 수집뿐입니다.