Core Simplifier
とりあえず、分からないものは放置の方向で!コメントでツッコミをもらって、GHCはやっぱり賢かったということに気付いたよ。最適化オプション付けると、全然ちげー。"main = return(1)"程度なら、型宣言してもしないでも、生成されるコードは一緒だとか、関数テーブル辿るの無駄っぽいなーと思ったのも、ちゃんと静的に解決されるとか、ぬー。
test = sin(2.3)+3.1 main = return(test)
こんなのを書くと、-fext-core出力が
lvl :: GHCziNum.Integer = GHCziNum.Szh (31::GHCziPrim.Intzh); lvl1 :: GHCziNum.Integer = GHCziNum.Szh (10::GHCziPrim.Intzh); lvl2 :: GHCziNum.Integer = GHCziNum.Szh (23::GHCziPrim.Intzh); x :: GHCziFloat.Double = %case (GHCziFloat.Double) (GHCziFloat.zdwzdsfromRat Main.lvl2 Main.lvl1) %of (wild::GHCziFloat.Double) {GHCziFloat.Dzh (x1::GHCziPrim.Doublezh) -> %case (GHCziFloat.Double) (GHCziFloat.zdwzdsfromRat Main.lvl Main.lvl1) %of (wild1::GHCziFloat.Double) {GHCziFloat.Dzh (y::GHCziPrim.Doublezh) -> GHCziFloat.Dzh (GHCziPrim.zpzhzh (GHCziPrim.sinDoublezh x1) y)}};
こんな感じになる。やった!これくらいなら手で書けるよ!けど、この浮動小数点の扱いはどうなんだろう。
"map f (map g x) -> map (f.g) x"程度の最適化も普通にされるっぽいなー。GHC.Base見ると、直接、こういうRULESは定義されてない。けど、
build :: forall a. (forall b. (a -> b -> b) -> b -> b) -> [a] {-# INLINE [1] build #-} -- The INLINE is important, even though build is tiny, -- because it prevents [] getting inlined in the version that -- appears in the interface file. If [] *is* inlined, it -- won't match with [] appearing in rules in an importing module. -- -- The "1" says to inline in phase 1 build g = g (:) [] ---------------------------------------------------------------------- {-# RULES "fold/build" forall k z (g::forall b. (a->b->b) -> b -> b) . foldr k z (build g) = g k z ---------------------------------------------------------------------- {-# RULES "map" [~1] forall f xs. map f xs = build (\c n -> foldr (mapFB c f) n xs) "mapList" [1] forall f. foldr (mapFB (:) f) [] = map f "mapFB" forall c f g. mapFB (mapFB c f) g = mapFB c (f.g) #-}
あたりを使えばできそう。
map f (map g xs) = {apply "map" RULES" twice} build (\c n -> foldr (mapFB c f) n (build (\c1 n1 -> foldr (mapFB c1 g) n1 xs))) = {apply "fold/build" RULES} build (\c n -> (\c1 n1 -> foldr (mapFB c1 g) n1 xs) (mapFB c f) n) = {beta-reduction.怪しい・・・} build (\c n -> (foldr (mapFB (mapFB c f) g) n xs)) = {apply "mapFB" RULES} build (\c n -> (foldr (mapFB c (f.g)) n xs)) = {build inlining} (foldr (mapFB (:) (f.g)) [] xs) = {apply "mapList" RULES} map (f.g) xs
実際GHCがどういう順序で計算してるかはわからんけど、RULESの適用順序が違うと、正しい結果に辿りつかなくなる可能性もあるような気がするけど、そのへんはどうしてるんだろうか
{-# RULES "length/map" forall f x. length (map f x) = length x #-}
みたいな最適化は、GHCではされないよう。
lengthの定義(base/GHC/List.lhs)は返り値がInt型なので、あれだけど、genericLengthとかあるらしいので、Data.Listを見る。特に、RULESは定義されてないし、genericLength使っても、やっぱしmap消去はされないっぽい。
てことで、自前でLengthを定義
{-# OPTIONS -fglasgow-exts #-} myLength :: (Num i) => [b] -> i myLength = foldr (\_ -> (+1)) 0 test = myLength (map (+1) [1,2,3,4,5]) main = return test
なんか、これだけでいけそうな気がしたので、-O付けて、-ddump-simpl
Rec { Main.go :: [GHC.Num.Integer] -> GHC.Num.Integer [GlobalId] [Arity 1 NoCafRefs Str: DmdType S] Main.go = \ (ds_a1Nm :: [GHC.Num.Integer]) -> case GHC.Num.Integer ds_a1Nm of wild_a29v { [] -> z_r2ax; : y_a29t ys_a29u -> GHC.Num.plusInteger (Main.go ys_a29u) Main.lit } end Rec } ---------------------------------------- Main.x :: GHC.Num.Integer [GlobalId] [Str: DmdType] Main.x = Main.go Main.lvl8
長いので途中すっとばしたけど、Main.goがmyLength相当の関数で(goはfoldrの定義内部で使われてる関数だと思う)、Main.xがtest相当の変数っぽいので、map消去できてる。うーむ、あっさりできたな。GHCが確実にmyLengthを展開してくれるかどうかはわからんので、一応INLINE指定しておいた方がいいのかな。
標準ライブラリが、foldr使って定義してないのは、末尾再帰じゃないからとか、そういう話なのかねー。と思ったが、"foldr (\_ -> (+1)) 0" だけでなく、"foldl (\n _ -> n+1) 0" , genericLengthも長さ100万~1000万程度のリストでスタックオーバーフロー起こすな。lengthの定義は
length :: [a] -> Int length l = len l 0# where len :: [a] -> Int# -> Int len [] a# = I# a# len (_:xs) a# = len xs (a# +# 1#)
となってる。Int#が、即値みたいな感じなのかね(Peyton Jonesの論文に、unboxed typeとか書いてあったけど)。だとすると、Integer使う限り、どうしようもないのかも。でも、genericLengthを普通のlengthにしといて、今のlengthは、別扱いにするのが、Haskell的設計なんでないかという気はするけど
フフ ・・・へただなあ・・・カイジ君・・・へたっぴさ・・・・欲望の開放のさせ方がへた・・・ カイジ君が本当に使いたいのは・・・こっちのfoldr・・・!これを融合変換使って・・・・中間データを一気に消去してさ・・・・効率化に頭を悩まさずコードを書きたい・・・・・・・・!だろ・・・・・?だけど・・それはあまりにも高価だから、こっちの・・・Int#使ったlengthでごまかそうって言うんだ・・・・カイジくん・・・ダメなんだよ・・・!そういうのが実にダメ・・!その妥協は痛ましすぎる・・・
一方のgenericLength見ると
genericLength :: (Num i) => [b] -> i genericLength [] = 0 genericLength (_:l) = 1 + genericLength l
と定義されている。いちいちfoldr使って〜とか意識すんのはだるいし、こっから適当に「ラムダ消去」して、foldr使った定義導けると嬉しいわけだけど、そういうのって何とかならんのかねー
あとまあ、どうでもよいが、コンパイラのソースをGHCから気軽に利用できるようになっているといいなーと思った。CoreやStgのダンプが見づらいから自分専用の出力フォーマット定義したいとか、そういう程度の理由なんだけど。