リストのほとんどがすぐに役に立たなくなるのに、なぜ GC は計算中にリストの先頭を処理しないのですか?
fibs1
大量のメモリを使用し、scanl
怠惰なため遅いです。リスト要素を評価しないため、
fibs1' = 0 : scanl (+) 1 fibs1'
生産する
0 : scanl (+) 1 (0 : more)
0 : 1 : let f2 = 1+0 in scanl (+) f2 (1 : more')
0 : 1 : let f2 = 1+0 in f2 : let f3 = f2+1 in scanl (+) f3 (f2 : more'')
0 : 1 : let f2 = 1+0 in f2 : let f3 = f2+1 in f3 : let f4 = f3+f2 in scanl (+) f4 (f3 : more''')
など。そのため、入れ子になった巨大なサンクをすぐに取得できます。そのサンクが評価されると、スタックにプッシュされ、250000 から 350000 の間のある時点で、デフォルトのスタックには大きすぎます。
また、各リスト要素は、評価されていない間、前の要素への参照を保持するため、リストの先頭をガベージ コレクションすることはできません。
厳密なスキャンを使用する場合、
fibs1 :: Int -> Integer
fibs1 n = fibs1' !! n
where
fibs1' = 0 : scanl' (+) 1 fibs1'
scanl' f a (x:xs) = let x' = f a x in x' `seq` (a : scanl' f x' xs)
scanl' _ a [] = [a]
番目のリストセルが生成されるときk
、その値はすでに評価されているため、前のセルを参照していません。したがって、リストをトラバースするときに、リストをガベージコレクションすることができます (他に何も参照を保持していないと仮定します)。
その実装では、リストバージョンはほぼ同じくらい高速で無駄fib2
がありません(それでもリストセルを割り当てる必要があるため、割り当てが少し多くなり、おそらく少し遅くなりますが、違いはごくわずかです。フィボナッチ数リスト構築のオーバーヘッドが無視できるほど大きくなる)。
の考え方scanl
は、その結果が段階的に消費されるため、消費によって要素が強制され、大きなサンクの蓄積が防止されるというものです。
一度に2つの要素しか必要とされないのに、なぜGHCはリストバージョンを変数バージョンに最適化しないのですか?
そのオプティマイザーは、アルゴリズムを見てそれを判断することはできません。scanl
コンパイラには不透明で、何をするかわかりませんscanl
。
正確なソースコードを取得するとscanl
(名前を変更するかscanl
、プレリュードから非表示にするか、名前を変更することを選択しました)、
scans :: (b -> a -> b) -> b -> [a] -> [b]
scans f q ls = q : (case ls of
[] -> []
x:xs -> scans f (f q x) xs)
モジュールをエクスポートしてコンパイルし (-O2 を使用)、生成されたインターフェイス ファイルを次のコマンドで確認します。
ghc --show-iface Scan.hi
(たとえば、コンパイラのバージョン間の小さな違い)
Magic: Wanted 33214052,
got 33214052
Version: Wanted [7, 0, 6, 1],
got [7, 0, 6, 1]
Way: Wanted [],
got []
interface main:Scan 7061
interface hash: ef57dac14815e2f1f897b42a007c0c81
ABI hash: 8cfc8dab79de6a51fcad666f1869574f
export-list hash: 57d6805e5f0b5f76f0dd8dfb228df988
orphan hash: 693e9af84d3dfcc71e640e005bdc5e2e
flag hash: 1e8135cb44ef6dd330f1f56943d1f463
used TH splices: False
where
exports:
Scan.scans
module dependencies:
package dependencies: base* ghc-prim integer-gmp
orphans: base:GHC.Base base:GHC.Float base:GHC.Real
family instance modules:
import -/ base:Prelude 1cb4b618cf45281dc97748b1831bf0cd
d79ca4e223c0de0a770a3b88a5e67687
scans :: forall b a. (b -> a -> b) -> b -> [a] -> [b]
{- Arity: 3, HasNoCafRefs, Strictness: LLL -}
vectorised variables:
vectorised tycons:
vectorised reused tycons:
scalar variables:
scalar tycons:
trusted: safe-inferred
require own pkg trusted: False
インターフェイス ファイルが関数の展開を公開せず、その型、アリティ、正格性のみを公開し、CAF を参照していないことを確認します。
モジュールのインポートがコンパイルされると、コンパイラーが通過しなければならないのは、インターフェースファイルによって公開された情報だけです。
ここでは、コンパイラが関数の呼び出しを発行する以外のことを実行できるようにする情報は公開されていません。
展開が公開された場合、コンパイラは展開をインライン化し、型と組み合わせ関数を認識してコードを分析し、サンクを構築しないより熱心なコードを生成する機会がありました。
ただし、のセマンティクスscanl
は最大限に遅延しており、入力リストが検査される前に出力の各要素が発行されます。その結果、リストに未定義の値が含まれていると結果が変わるため、GHC は加算を厳密にすることができません。
scanl (+) 1 [undefined] = 1 : scanl (+) (1 + undefined) [] = 1 : (1 + undefined) : []
その間
scanl' (+) 1 [undefined] = let x' = 1 + undefined in x' `seq` 1 : scanl' (+) x' []
= *** Exception: Prelude.undefined
バリアントを作成できます
scanl'' f b (x:xs) = b `seq` b : scanl'' f (f b x) xs
上記の入力に対して生成1 : *** Exception: Prelude.undefined
されますが、リストに未定義の値が含まれている場合、厳格さによって結果が実際に変更されるため、コンパイラーが展開を知っていたとしても、未定義の値がないことを証明できない限り、評価を厳密にすることはできませんでした。これは私たちには明らかな事実ですが、コンパイラーにはわかりません [コンパイラーにそれを認識させ、未定義の値がないことを証明できるようにするのは簡単ではないと思います]。