[解決済み] foldlは末尾再帰的なのに、なぜfoldrはfoldlより高速に動作するのか?
質問
私はfoldlとfoldrを比較テストしたいと思いました。私が見たところでは、末尾再帰最適化のために、できる限りfoldrよりもfoldlを使用するべきです。
これは理にかなっています。しかし、このテストを実行した後、私は混乱しています。
foldr (timeコマンドを使用した場合、0.057秒かかります)。
a::a -> [a] -> [a]
a x = ([x] ++ )
main = putStrLn(show ( sum (foldr a [] [0.. 100000])))
foldl (timeコマンド使用時0.089秒かかる)。
b::[b] -> b -> [b]
b xs = ( ++ xs). (\y->[y])
main = putStrLn(show ( sum (foldl b [] [0.. 100000])))
この例が些細なものであることは明らかですが、なぜfoldrがfoldlに勝っているのか、私は混乱しています。これはfoldlが勝つ明確なケースであるべきではないでしょうか?
どのように解決するのですか?
怠惰な評価の世界へようこそ。
厳密な評価という観点で考えると、foldlは末尾再帰的なので、foldlは"good"、foldrは"bad"に見えますが、foldrは最後のアイテムを最初に処理できるようにスタックの中にタワーを作らなければならないでしょう。
しかし、遅延評価は状況を一変させます。 例えば、map関数の定義を見てみましょう。
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Haskellが厳密な評価を行う場合、まず末尾を計算し、それから(リスト内のすべての項目に対して)項目を前置しなければならないので、これはあまり良いことではないでしょう。 これを効率的に行う唯一の方法は、要素を逆に構築することであると思われます。
しかし、Haskellの遅延評価のおかげで、このマップ関数は実際に効率的です。 Haskell のリストは生成器と考えることができ、このマップ関数は入力リストの最初のアイテムに f を適用することによって最初のアイテムを生成します。 2番目の項目が必要なときは、同じことをもう一度するだけです(余分なスペースは使いません)。
これは
map
は、以下のように記述することができます。
foldr
:
map f xs = foldr (\x ys -> f x : ys) [] xs
見た目にはわかりにくいですが、遅延評価はfoldrが
f
をすぐに与えることができるからです。
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
なぜなら
f
で定義される
map
は最初のパラメータだけを使って結果リストの最初の項目を返すことができるので、foldは定数空間で遅延的に動作することができます。
さて、遅延評価には問題があります。 たとえば、sum [1..1000000] を実行してみてください。 これはスタックオーバーフローを発生させます。 なぜでしょうか? 左から右へ評価すればいいだけでしょう?
Haskellがどのようにそれを評価するか見てみましょう。
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
sum = foldl (+) 0
sum [1..1000000] = foldl (+) 0 [1..1000000]
= foldl (+) ((+) 0 1) [2..1000000]
= foldl (+) ((+) ((+) 0 1) 2) [3..1000000]
= foldl (+) ((+) ((+) ((+) 0 1) 2) 3) [4..1000000]
...
= (+) ((+) ((+) (...) 999999) 1000000)
Haskellは足し算を行うにはあまりに怠惰です。 その代わりに、評価されないサンクの塔ができあがり、それは数を得るために強制的に評価されなければなりません。 すべてのサンクを評価するために深く再帰しなければならないので、スタックオーバーフローはこの評価の間に発生します。
幸いなことに、Data.Listには
foldl'
という特殊な関数があり、これが厳密に動作します。
foldl' (+) 0 [1..1000000]
はスタックオーバーフローを起こしません。 (注意: 私は
foldl
を
foldl'
を追加しましたが、実際には実行速度が遅くなりました)。
関連
-
[解決済み] SQLiteのINSERT/per-secondのパフォーマンスを向上させる
-
[解決済み] Collatz予想の検証を行うC++のコードは、なぜ手書きのアセンブリよりも高速に動作するのでしょうか?
-
[解決済み] GHCはどのような最適化を確実に実行することが期待できますか?
-
[解決済み] foldrとfoldl(またはfoldl')の意味するところ
-
[解決済み] 無限リストでのfoldlとfoldrの動作
-
[解決済み] Haskellには末尾再帰的最適化があるか?
-
[解決済み] VBAコードの実行時間をどのようにテストしますか?
-
[解決済み] GCC: marchとmtuneはどう違うのですか?
-
[解決済み] 最適化はいつから始まるのか?
-
[解決済み] foldrはどのように機能するのですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] コピーオンライトとは何ですか?
-
[解決済み] CPUバウンド」「I/Oバウンド」とは、どのような意味ですか?
-
[解決済み] GHCはどのような最適化を確実に実行することが期待できますか?
-
[解決済み] 本番用Webアプリケーションの1秒あたりの「平均」リクエスト数は?
-
[解決済み] なぜJava APIはshortやbyteの代わりにintを使うのですか?
-
[解決済み] ロガー slf4j 文字列の連結の代わりに{}でフォーマットすることの利点
-
[解決済み] JPEG最適化のためのツール?[クローズド]
-
[解決済み] Haskellには末尾再帰的最適化があるか?
-
[解決済み] VBAコードの実行時間をどのようにテストしますか?
-
[解決済み] 最適化はいつから始まるのか?