GHC Haskell에서 메모는 언제 자동으로 이루어 집니까?


106

m2가 다음에 없는데 m1이 분명히 메모 된 이유를 알 수 없습니다.

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10000000은 첫 번째 호출에서 약 1.5 초가 걸리고 후속 호출에서 그 일부 (아마도 목록을 캐시 함)에 걸리는 반면, m2 10000000은 항상 같은 시간 (각 호출로 목록을 다시 작성)이 걸립니다. 무슨 일인지 아십니까? GHC가 함수를 메모할지 여부와시기에 대한 경험 규칙이 있습니까? 감사.

답변:


112

GHC는 기능을 메모하지 않습니다.

그러나 주변 람다식이 입력 될 때마다 최대 한 번 또는 최상위 수준에있는 경우 최대 한 번 코드에서 주어진 식을 계산합니다. 예제에서와 같이 구문 설탕을 사용할 때 람다 표현식이 어디에 있는지 결정하는 것은 약간 까다로울 수 있으므로이를 동등한 desugared 구문으로 변환 해 보겠습니다.

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n

(참고 : Haskell 98 보고서는 실제로 왼쪽 연산자 섹션 (a %)을에 해당 하는 것으로 설명 \b -> (%) a b하지만 GHC는이를으로 탈당합니다 (%) a. 이들은 다음과 같이 구분 될 수 있기 때문에 기술적으로 다릅니다.seq 내가 이것에 대해 GHC Trac에 티켓을 제출 한 것 같아요..)

이 감안할 때, 당신이에서 볼 수 m1', 표현은 filter odd [1..]그것이 단지 프로그램의 실행에 한 번 계산 될 수 있도록에있는 동안, 어떤 람다 표현에 포함되지 않은 m2', filter odd [1..]람다 표현이 입력 될 때마다 계산됩니다, 즉, 의 각 호출에서 m2'. 그것은 당신이보고있는 타이밍의 차이를 설명합니다.


실제로 특정 최적화 옵션이있는 일부 GHC 버전은 위의 설명이 나타내는 것보다 더 많은 값을 공유합니다. 이것은 일부 상황에서 문제가 될 수 있습니다. 예를 들어, 함수를 고려하십시오.

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])

GHC 는 함수 y에 의존하지 않고 x다시 작성하는 것을 알 수 있습니다.

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

이 경우 새 버전은 y저장된 메모리에서 약 1GB를 읽어야 하므로 원래 버전은 일정한 공간에서 실행되고 프로세서 캐시에 맞기 때문에 훨씬 덜 효율적 입니다. 실제로 GHC 6.12.1에서이 함수 f는 최적화 없이 컴파일 할 때 -O2.


1
평가 비용 (필터 홀수 [1 ..]) 표현식은 어쨌든 0에 가깝습니다. 결국 게으른 목록이므로 목록이 실제로 평가 될 때 실제 비용은 (x !! 10000000) 응용 프로그램에 있습니다. 게다가, m1과 m2는 적어도 다음 테스트 내에서 -O2와 -O1 (내 ghc 6.12.3에서)으로 한 번만 평가되는 것 같습니다 : (테스트 = m1 10000000 seqm1 10000000). 최적화 플래그가 지정되지 않은 경우 차이가 있습니다. 그리고 "f"의 두 변형 모두 최적화에 관계없이 최대 5356 바이트의 상주를 갖습니다 (-O2가 사용될 때 총 할당량이 적음).
Ed'ka

1
@ Ed'ka : 위의 정의와 함께이 테스트 프로그램을 시도해보십시오 f: main = interact $ unlines . (show . map f . read) . lines; 유무에 관계없이 컴파일 -O2; 그런 다음 echo 1 | ./main. 당신이 테스트처럼 작성하는 경우 main = print (f 5), 다음 y이 사용되는 쓰레기 수집 둘 개 사이에 아무런 차이가 없다 할 수 있습니다 f들.
Reid Barton

어, map (show . f . read)당연히 이어야합니다 . 이제 GHC 6.12.3을 다운로드 했으므로 GHC 6.12.1과 동일한 결과를 볼 수 있습니다. 그리고 네, 당신은 바로 원본에 대한 있습니다 m1m2변형시킬 것이다 활성화 최적화를 리프팅의이 종류를 수행 GHC 버전 : m2m1.
Reid Barton

예, 이제 차이점이 보입니다 (-O2가 확실히 느립니다). 이 예에 감사드립니다!
Ed'ka

29

m1은 상수 적용 형식이기 때문에 한 번만 계산되고 m2는 CAF가 아니므로 각 평가에 대해 계산됩니다.

CAF에 대한 GHC wiki를 참조하십시오 : http://www.haskell.org/haskellwiki/Constant_applicative_form


1
“m1은 상수 적용 형이기 때문에 한 번만 계산됩니다.”라는 설명이 이해가되지 않습니다. 아마도 m1과 m2는 모두 최상위 변수이기 때문에 이러한 함수 는 CAF인지 여부에 관계없이 한 번만 계산 된다고 생각합니다 . 차이점은 목록 [1 ..]이 프로그램 실행 중에 한 번만 계산 되는지 아니면 함수의 응용 프로그램 당 한 번 계산 되는지 여부입니다. 하지만 CAF와 관련이 있습니까?
Tsuyoshi Ito

1
링크 된 페이지에서 : "CAF ... 모든 사용자가 공유 할 그래프 조각으로 컴파일하거나 처음 평가할 때 일부 그래프로 자신을 덮어 쓰는 공유 코드로 컴파일 할 수 있습니다." m1CAF 이므로 두 번째가 적용되고 filter odd [1..](단지 [1..]!가 아니라) 한 번만 계산됩니다. GHC는를 m2참조하고 filter odd [1..]에서 사용 된 동일한 썽크에 대한 링크를 배치 할 수도 m1있지만 이는 나쁜 생각입니다. 일부 상황에서 큰 메모리 누수가 발생할 수 있습니다.
Alexey Romanov

@Alexey : [1..]및 에 대한 수정에 감사드립니다 filter odd [1..]. 나머지는 여전히 확신 할 수 없습니다. 내가 착각하지 않았다면 CAF는 컴파일러 filter odd [1..] in m2을 전역 썽 크로 대체 할 수 있다고 주장 할 때만 관련 이 있습니다 (에서 사용 된 것과 동일한 썽 크일 수도 있음 m1). 그러나 질문자의 상황에서 컴파일러는 "최적화"를 수행 하지 않았 으며 질문과의 관련성을 볼 수 없습니다.
Tsuyoshi Ito

2
에서 대체 할 수 있다는 점과 관련이 있습니다 m1.
Alexey Romanov

13

두 가지 형태 사이에는 결정적인 차이가 있습니다. m2가 명시 적으로 인수를 제공했기 때문에 단 형성 제한이 m1에는 적용되지만 m2에는 적용되지 않습니다. 따라서 m2의 유형은 일반적이지만 m1은 구체적입니다. 할당 된 유형은 다음과 같습니다.

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

대부분의 Haskell 컴파일러와 인터프리터 (실제로 알고있는 모든 것)는 다형성 구조를 기억하지 않으므로 m2의 내부 목록은 호출 될 때마다 다시 생성됩니다. 여기서 m1은 그렇지 않습니다.


1
GHCi에서 이것들을 가지고 노는 것은 또한 let-floating 변환 (GHCi에서 사용되지 않는 GHC의 최적화 패스 중 하나)에 의존하는 것 같습니다. 물론 이러한 간단한 함수를 컴파일 할 때 옵티마이 저는 그것들이 어쨌든 동일하게 작동하도록 만들 수 있습니다 (어쨌든 실행 한 일부 기준 테스트에 따르면 별도의 모듈에있는 함수와 NOINLINE pragma로 표시됨). 아마도 그것은 목록 생성과 인덱싱이 어쨌든 매우 엄격한 루프로 융합되기 때문일 것입니다.
mokus

1

나는 Haskell을 처음 접했기 때문에 확실하지 않지만 두 번째 함수는 매개 변수화되고 첫 번째 함수는 매개 변수화되지 않았기 때문인 것 같습니다. 함수의 특성은 입력 값에 따라 결과가 달라지고 특히 기능 패러다임에서는 입력에만 의존한다는 것입니다. 분명한 의미는 매개 변수가없는 함수는 어떤 일이 있어도 항상 동일한 값을 반환한다는 것입니다.

GHC 컴파일러에는이 사실을 이용하여 전체 프로그램 런타임에 대해 한 번만 이러한 함수의 값을 계산하는 최적화 메커니즘이 있습니다. 확실히 게으른 일이지만 그럼에도 불구하고 그렇게합니다. 다음 함수를 작성했을 때 직접 알아 차 렸습니다.

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0

그런 다음 그것을 테스트하기 위해 GHCI에 들어가 다음과 같이 썼습니다 primes !! 1000. 몇 초가 걸렸지 만 마침내 답을 얻었습니다 7927. 그러다 전화를 걸어 primes !! 1001즉시 답변을 받았습니다. 마찬가지로 즉시 결과를 얻었습니다. take 1000 primesHaskell은 이전에 1001 번째 요소를 반환하기 위해 전체 1,000 개 요소 목록을 계산해야했기 때문입니다.

따라서 매개 변수를 사용하지 않도록 함수를 작성할 수 있다면 아마도 그것을 원할 것입니다. ;)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.