이 피보나치 함수는 어떻게 메모됩니까?


114

이 피보나치 함수는 어떤 메커니즘으로 기억됩니까?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

관련 메모에서이 버전이 아닌 이유는 무엇입니까?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

13
약간 무관하며 fib 0종료하지 않습니다. 기본 케이스 fib'fib' 0 = 0fib' 1 = 1.
후온

1
첫 번째 버전은 더 간결하게 만들 수 있습니다 : fibs = 1:1:zipWith (+) fibs (tail fibs)fib = (fibs !!).
Bastian

답변:


95

하스켈 평가 메커니즘은 별로 필요가 값이 필요할 때, 그것을 계산하고 다시 요구되는 경우에 준비가 유지된다. 일부 목록을 정의 xs=[0..]하고 나중에 100 번째 요소 인을 요청 xs!!99하면 목록의 100 번째 슬롯이 "살아남" 99상태가되어 현재 번호를 보유하고 다음 액세스를 준비합니다.

이것이 바로 "목록을 통과하는"트릭이 악용하는 것입니다. 정상적인 이중 재귀 피보나치 정의 fib n = fib (n-1) + fib (n-2)에서 함수 자체가 맨 위에서 두 번 호출되어 지수 폭발을 일으 킵니다. 그러나 그 트릭으로 우리는 중간 결과에 대한 목록을 설정하고 "목록을 통해"진행합니다.

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

트릭은 해당 목록을 생성하고 해당 목록이를 호출하는 사이에 가비지 수집을 통해 사라지지 않도록하는 것 fib입니다. 이를 달성하는 가장 쉬운 방법 은 목록의 이름지정 하는 것입니다. "네가 이름을 지으면 그대로 남아있을 것이다."


첫 번째 버전은 단형 상수를 정의하고 두 번째 버전은 다형성 함수를 정의합니다. 다형성 함수는 제공해야 할 다른 유형에 대해 동일한 내부 목록을 사용할 수 없으므로 공유 , 즉 메모가 없습니다.

첫 번째 버전에서 컴파일러는 상수 하위 표현식 ( )을 제거하고이를 별도의 공유 가능한 엔티티로 만드는 등 관대map fib' [0..] 하지만 그렇게 할 의무는 없습니다. 실제로 우리 자동으로 수행하는 것을 원하지 않는 경우가 있습니다.

( 편집 :) 다음 재 작성을 고려하십시오.

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

따라서 실제 이야기는 중첩 된 범위 정의에 관한 것 같습니다. 첫 번째 정의에는 외부 범위가 없으며 세 번째는 외부 범위를 호출하지 않고 fib3동일한 수준 을 호출하도록주의 합니다 f.

의 값에 따라 (이론상) 다르게 정의 될 fib2 있기 때문에 각각의 새로운 호출은 중첩 된 정의를 새로 만드는 것 같습니다 ( 이를 지적한 Vitus와 Tikhon에게 감사드립니다). 첫 번째 (고화질)로 더 없다 에 의존하고, 세 번째에 의존하지만 각각 별도의 호출이 로 호출 이 특정 호출 내부와 동일한 수준의 범위에서만 호출 정의에주의 인 동일하므로, 도착 해당 호출에 재사용 (즉 공유)됩니다 .nnfib3ffib3xsfib3

그러나 컴파일러가 위의 모든 버전의 내부 정의가 실제로 외부 바인딩과 독립적 이라는 것을 인식하는 것을 배제하지 않고 n결국 람다 리프팅 을 수행하여 전체 메모 화 (다형성 정의 제외)를 수행합니다. 사실 이것은 단형 유형으로 선언되고 -O2 플래그로 컴파일 될 때 세 가지 버전 모두에서 정확히 발생합니다. 다형성 유형 선언을 사용하면 fib3로컬 공유를 표시하고 fib2공유하지 않습니다.

궁극적으로 컴파일러 및 사용 된 컴파일러 최적화, 테스트 방법 (GHCI에서 파일로드, 컴파일 여부, -O2 사용 여부, 독립 실행 형), 모노 모픽 또는 다형성 유형을 가져 오는지 여부에 따라 동작이 수행 될 수 있습니다. 완전히 변경-로컬 (호출 당) 공유 (즉, 각 호출에 대한 선형 시간), 메모 (즉, 첫 번째 호출에 선형 시간, 후속 호출에 동일하거나 더 작은 인수가있는 후속 호출에 0 시간)를 표시하는지 또는 전혀 공유하지 않는지 ( 지수 시간).

짧은 대답은 컴파일러라는 것입니다. :)


4
그냥 약간의 세부 사항을 해결하기 : 두 번째 버전은 로컬 기능을 주로하기 때문에 어떤 공유하지 않는 fib'모든 재정의되고 n, 따라서와 fib'fib 1fib'fib 2또한 목록이 다른 의미하는가. 유형을 단형으로 수정하더라도 여전히이 동작을 나타냅니다.
Vitus

1
where절은 let표현 과 매우 유사한 공유를 도입 하지만 이와 같은 문제를 숨기는 경향이 있습니다. : 더 명시 적으로 조금 재 작성, 당신이 얻을 hpaste.org/71406
비투스

1
당신의 재 작성에 대한 또 다른 흥미로운 점 : 당신이 그 (것)들에게 단형 유형 (예를주는 경우 Int -> Integer), 다음 fib2지수 시간에 실행 fib1fib3선형 시간에 모두 실행하지만, fib1또한 memoized됩니다 - 다시 때문에 대한 fib3로컬 정의는 모든 재정의된다 n.
Vitus

1
@misterbee하지만 실제로 컴파일러로부터 어떤 종류의 보증을받는 것이 좋을 것입니다. 특정 엔티티의 메모리 상주에 대한 일종의 제어. 때때로 우리는 공유를 원하고 때로는 그것을 방지하고 싶습니다. 나는 그것이 가능해야한다고 상상 / 희망한다 ...
Will Ness 2014 년

1
@ElizaBrandt 제가 의미하는 것은 때때로 무거운 것을 다시 계산하여 메모리에 유지되지 않도록하는 것입니다. 즉, 다시 계산하는 비용이 엄청난 메모리 유지 비용보다 낮습니다. 한 가지 예는 powerset 생성입니다. 독립적으로 두 번 계산 pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]되기를 원 pwr xs하므로 생산 및 소비되는 즉시 가비지 수집이 가능합니다.
윌 네스

23

나는 완전히 확신하지 못하지만 여기에 교육받은 추측이 있습니다.

컴파일러는 이것이 fib n다를 수 있다고 가정 n하므로 매번 목록을 다시 계산해야합니다. where명령문 내부의 비트는 결국에 의존 할 수n 있습니다. 즉,이 경우 전체 숫자 목록은 본질적으로의 함수입니다 n.

없는 버전 n은 목록을 한 번 생성하고 함수로 래핑 할 수 있습니다. 이 목록 n 전달 된 값에 의존 할 수 없으며 확인하기 쉽습니다. 목록은 색인화되는 상수입니다. 물론 이것은 느리게 평가되는 상수이므로 프로그램은 전체 (무한) 목록을 즉시 가져 오려고하지 않습니다. 상수이기 때문에 함수 호출에서 공유 할 수 있습니다.

재귀 호출은 목록에서 값을 조회하기 만하면되기 때문에 전혀 기억되지 않습니다. fib버전은 한 번 느리게 목록을 생성 하므로 중복 계산을 수행하지 않고 답을 얻을 수있을만큼만 계산합니다. 여기서 "lazy"는 목록의 각 항목이 썽크 (평가되지 않은 표현식)임을 의미합니다. 당신이하면 않는 썽 크는를 평가, 그렇게 그에게 어떤 계산을 반복 않습니다 다음에 액세스 값이된다. 목록은 통화간에 공유 할 수 있으므로 이전 항목은 모두 다음 항목이 필요한 시간에 이미 계산됩니다.

본질적으로 GHC의 게으른 의미 체계를 기반으로 한 영리하고 임대료가 낮은 동적 프로그래밍 형식입니다. 나는 표준 이 엄격하지 않아야한다고 만 명시하고 있으므로 준수 컴파일러는 잠재적으로이 코드를 메모 하지 않기 위해 컴파일 할 수 있습니다 . 그러나 실제로 모든 합리적인 컴파일러는 게으르다.

두 번째 사례가 작동하는 이유에 대한 자세한 내용은 재귀 적으로 정의 된 목록 이해 (zipWith 측면에서 fibs)를 읽어보세요 .


" fib' n다른 사람이 다를 수있다 " 는 뜻 n인가요?
Will Ness

나는 명확하지 않다고 생각한다. 내가 의미하는 바는를 fib포함하여 내부의 모든 fib'것이 각기 다를 수 있다는 것 n입니다. 나는 fib'또한 n다른 그림자 를 그 자체에 의존 하기 때문에 원래의 예가 약간 혼란 스럽다고 생각합니다 n.
Tikhon Jelvis

20

첫째,로 컴파일 된 ghc-7.4.2를 사용하면 -O2메모리가없는 버전이 그렇게 나쁘지 않습니다. 피보나치 수의 내부 목록은 함수에 대한 각 최상위 호출에 대해 여전히 메모됩니다. 그러나 다른 최상위 수준의 호출에 대해 메모 할 수 없으며 합리적으로 저장할 수도 없습니다. 그러나 다른 버전의 경우 목록이 통화간에 공유됩니다.

이는 단 형성 제한 때문입니다.

첫 번째는 단순 패턴 바인딩 (이름 만, 인수 없음)에 의해 바인딩되므로 단일형 제한에 따라 단일형 유형을 가져와야합니다. 추론 된 유형은 다음과 같습니다.

fib :: (Num n) => Int -> n

이러한 제약 조건은 기본 선언이 없으면으로 기본 설정 Integer되어 형식을 다음과 같이 수정합니다.

fib :: Int -> Integer

따라서 [Integer]메모 할 목록 (유형 )이 하나뿐입니다.

두 번째는 함수 인수로 정의되므로 다형성으로 유지되며 내부 목록이 호출간에 메모 된 경우 .NET의 각 유형에 대해 하나의 목록을 메모해야합니다 Num. 실용적이지 않습니다.

단 형성 제한을 비활성화하거나 동일한 유형 서명을 사용하여 두 버전을 모두 컴파일하고 둘 다 정확히 동일한 동작을 나타냅니다. (이전 컴파일러 버전에서는 사실이 아니 었습니다. 어떤 버전이 먼저 실행되었는지 모르겠습니다.)


각 유형에 대한 목록을 메모하는 것이 비현실적인 이유는 무엇입니까? 원칙적으로 GHC는 런타임 중에 발생한 각 Num 유형에 대해 부분적으로 계산 된 목록을 포함하는 사전 (유형 클래스 제한 함수 호출과 유사)을 만들 수 있습니까?
misterbee 2014 년

1
@misterbee 원칙적으로 그럴 수 있지만 프로그램 fib 1000000이 많은 유형을 호출 하면 엄청난 메모리를 먹습니다. 이를 방지하려면 캐시가 너무 커질 때 목록을 버리는 휴리스틱이 필요합니다. 그리고 그러한 메모 화 전략은 아마도 다른 함수 나 값에도 적용될 것입니다. 따라서 컴파일러는 잠재적으로 많은 유형에 대해 메모하기 위해 잠재적으로 많은 수의 것들을 처리해야 할 것입니다. 합리적으로 좋은 휴리스틱으로 (부분적인) 다형성 메모를 구현하는 것이 가능할 것이라고 생각하지만 그만한 가치가있을 것 같지 않습니다.
Daniel Fischer

5

Haskell에는 메모 기능이 필요하지 않습니다. 경험적 프로그래밍 언어에만 해당 기능이 필요합니다. 그러나 Haskel은 기능적 언어이며 ...

그래서 이것은 매우 빠른 피보나치 알고리즘의 예입니다.

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipWith는 표준 Prelude의 기능입니다.

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

테스트:

print $ take 100 fib

산출:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

경과 시간 : 0.00018 초


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