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


106

m2が以下に含まれていないのにm1が明らかにメモ化されている理由がわかりません。

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

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

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

回答:


112

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

ただし、コード内の指定された式は、周囲のラムダ式が入力されるたびに1回だけ計算されます。最上位の場合は、最大で1回計算されます。例のように構文シュガーを使用する場合、ラムダ式がどこにあるかを判断するのは少しトリッキーになる可能性があるので、これらを同等の脱糖構文に変換してみましょう。

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

(注:Haskellの98のレポート、実際のように左オペレータセクション記述(a %)と同等にし\b -> (%) a bますが、GHCはそれをdesugars (%) 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、格納されているメモリから約1 GBを読み取る必要があるため、効率が大幅に低下しますが、元のバージョンは一定の領域で実行され、プロセッサのキャッシュに収まります。実際、GHC 6.12.1では、関数fは、最適化なしでコンパイルした場合、でコンパイルした場合よりも約2倍高速-O2です。


1
(フィルター奇数[1 ..])式を評価するコストはとにかくゼロに近いです-結局のところそれは遅延リストなので、リストが実際に評価されるときの実際のコストは(x !! 10000000)アプリケーションにあります。さらに、m1とm2の両方が-O2と-O1(私のghc 6.12.3)では少なくとも次のテスト内で1回だけ評価されるようです(テスト= m1 10000000 seqm1 10000000)。ただし、最適化フラグが指定されていない場合は違いがあります。ちなみに、 "f"の両方のバリアントは、最適化に関係なく、最大で5356バイトの常駐を持っています(-O2が使用されている場合、総割り当ては少なくなります)。
エドカ10/10/17

1
Ed'ka @:上記の定義と、このテストプログラムを試してみてくださいfmain = interact $ unlines . (show . map f . read) . lines。コンパイルの有無にかかわらず-O2; その後echo 1 | ./main。のようなテストを作成するmain = print (f 5)と、y使用時にガベージコレクションが行われ、2つfのに違いはありません。
リードバートン

ええ、map (show . f . read)もちろんそうです。GHC 6.12.3をダウンロードしたので、GHC 6.12.1と同じ結果が表示されます。そして、はい、あなたは右の元程度あるm1m2変えていく有効な最適化を持ち上げるのこの種を行うGHCのバージョン:m2m1
リードバートン

はい、今は違いがわかります(-O2は明らかに遅いです)。この例をありがとう!
エドカ

29

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

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


1
「m1は一定の適用形であるため、一度だけ計算される」という説明は私には意味がありません。おそらくm1とm2の両方がトップレベルの変数であるため、これらの関数はCAFであるかどうかに関係なく、一度だけ計算されると思います。違いは、リスト[1 ..]がプログラムの実行中に1回だけ計算されるか、関数のアプリケーションごとに1回計算されるかですが、CAFに関連していますか?
伊藤剛

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ます。しかし、質問者の状況では、コンパイラーはその「最適化」を行わなかったので、質問との関連性がわかりません。
伊藤剛

2
それを置き換えることができること関連している m1、そしてそれはありません。
Alexey Romanov

13

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

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

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


1
これらをGHCiで遊んでいると、それはlet-floating変換(GHCiでは使用されないGHCの最適化パスの1つ)にも依存しているようです。そしてもちろん、これらの単純な関数をコンパイルするとき、オプティマイザーは、とにかく同じように動作するようにすることができます(関数を別のモジュールに入れてNOINLINEプラグマでマークした、私がとにかく実行したいくつかの基準テストによると)。おそらくそれは、リストの生成とインデックス付けがとにかく非常にタイトなループに融合されるためです。
13:13

1

私自身は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 primesHaskellが前に1001番目の要素を返すために1000要素のリスト全体を計算する必要があったためです。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.