web-dev-qa-db-ja.com

GHC Haskellのメモ化はいつ自動化されますか?

M2が以下に含まれていないのにm1が明らかにメモ化されている理由を理解できません。

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

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

m1 10000000は最初の呼び出しで約1.5秒かかり、その後の呼び出しではその数分の1(おそらくリストをキャッシュする)ですが、m2 10000000では常に同じ時間がかかります(呼び出しごとにリストを再構築します)。何が起こっているのでしょうか? GHCが関数をメモするかどうか、いつメモするかについての経験則はありますか?ありがとう。

106
Jordan

GHCは機能を記憶しません。

ただし、コード内の任意の式は、周囲のラムダ式が入力されるたびに1回だけ計算されます。最上位の場合は、最大で1回計算されます。例のように構文シュガーを使用する場合、ラムダ式がどこにあるかを判断するのは少し難しいかもしれません。したがって、これらを同等の脱糖構文に変換しましょう。

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

(注:Haskell 98レポートは実際には_(a %)_のような左演算子セクションを\b -> (%) a bと同等であると説明していますが、GHCはそれを_(%) a_に変換します。これらは区別できるため、技術的に異なりますby seq。これについてのGHC Tracチケットを送信した可能性があります。)

これを考えると、_m1'_では、式_filter odd [1..]_はラムダ式に含まれていないため、プログラムの実行ごとに1回だけ計算されますが、_m2'_では、_filter odd [1..]_は、ラムダ式が入力されるたびに、つまり_m2'_が呼び出されるたびに計算されます。それはあなたが見ているタイミングの違いを説明しています。


実際、特定の最適化オプションを備えた一部のバージョンのGHCは、上記の説明が示すよりも多くの値を共有します。これは、状況によっては問題になることがあります。たとえば、関数を考えます

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

GHCはyxに依存していないことに気づき、関数を次のように書き換えます。

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

この場合、新しいバージョンはyが格納されているメモリから約1 GBを読み取る必要があるため、効率が大幅に低下しますが、元のバージョンは一定のスペースで実行され、プロセッサのキャッシュに収まります。実際、GHC 6.12.1では、関数fは、コンパイル時になし最適化を行うと、_-O2_を使用してコンパイルする場合よりも2倍高速になります。

111
Reid Barton

m1は定数適用形式であるため、1回だけ計算されますが、m2はCAFではないため、評価ごとに計算されます。

CAFのGHCウィキを参照してください: http://www.haskell.org/haskellwiki/Constant_applicative_form

29
sclv

2つの形式の間には決定的な違いがあります。単相性の制限はm1に適用されますが、m2には適用されません。したがって、m2のタイプは一般的ですが、m1のタイプは特定です。それらに割り当てられるタイプは次のとおりです。

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

ほとんどのHaskellコンパイラーとインタープリター(私が実際に知っているものすべて)はポリモーフィック構造を記憶していないため、m2の内部リストは呼び出されるたびに再作成されますが、m1はそうではありません。

13
mokus

私自身はHaskellに慣れていないのでわかりませんが、2番目の関数がパラメーター化されており、最初の関数はパラメーター化されていないためです。関数の性質は、その結果は入力値に依存し、関数パラダイムでは特に入力のみに依存するということです。明白な意味は、パラメータのない関数は、何があっても常に同じ値を繰り返し返すということです。

どうやら、この事実を利用してプログラム全体のランタイムに対して一度だけそのような関数の値を計算する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 primesの結果が得られました。これは、Haskellが1000要素のリスト全体を計算して、以前に1001番目の要素を返す必要があったためです。

したがって、パラメータを取らないように関数を記述できる場合は、おそらくそれが必要です。 ;)

1
Sventimir