我試圖找到 GHC 中發生的某種行內的來源,其中行內作為引數傳遞給另一個函式的函式。例如,我可能會撰寫如下定義(使用我自己的 List 型別以避免重寫規則):
data MyList a = Nil | Cons a (MyList a)
deriving (Show)
mapMyList :: (a -> b) -> MyList a -> MyList b
mapMyList f Nil = Nil
mapMyList f (Cons a rest) = Cons (f a) $ mapMyList f rest
接電話
fromList :: [a] -> MyList a
fromList = ...
main = do
print $ mapMyList (*2) $ fromList [1..5]
mapMyList是遞回的,所以它不能直接行內。但是,在生成的 Core 中,我看到以下定義:
Rec {
-- RHS size: {terms: 16, types: 11, coercions: 0, joins: 0/0}
Main.main_$smapMyList [Occ=LoopBreaker] :: MyList Int -> MyList Int
[GblId, Arity=1, Str=<S,1*U>, Unf=OtherCon []]
Main.main_$smapMyList
= \ (sc_s2Rb :: MyList Int) ->
case sc_s2Rb of {
Nil -> Main.Nil @Int;
Cons a_aBe rest_aBf ->
Main.Cons
@Int
(case a_aBe of { GHC.Types.I# x_a24u ->
GHC.Types.I# (GHC.Prim.*# x_a24u 2#)
})
(Main.main_$smapMyList rest_aBf)
}
end Rec }
尤其是smapMyList不再以函式為引數,(* 2)這里行內了。我想知道哪個優化通道生成了這段代碼,它是如何作業的?
我發現了這個相關的問題,它似乎要求一種使用 SPECIALIZE 編譯指示來保證這種行為的方法,這讓我相信專業化通行證正在這樣做。但是,在閱讀有關 GHC 專業化的檔案時,它似乎是為了專業化型別類字典,而不是函式引數(在我的示例中沒有型別類)。
我還查看了似乎相關的靜態引數轉換;例如,GHC 訊息來源對通行證是這樣說的:
可以看作是從回圈中移除不變數:在遞回呼叫中不會改變的遞回函式的引數從遞回中移除,這是在本地完成的,并且只傳遞有效改變的引數。
但是,我嘗試禁用這兩個傳遞,-fno-static-argument-transformation -fno-specialise發現無論如何這種轉換仍然發生。
我提出這個問題的動機是我正在用另一種語言 ( Koka )實作這種轉換,所以我想了解其他語言是如何做到的。
完整的測驗程式
生成的核心(禁用特化和靜態引數轉換后)
uj5u.com熱心網友回復:
這種優化被稱為“呼叫模式專業化”(又名 SpecConstr),它根據函式所應用的引數對函式進行專業化。優化在Simon Peyton Jones的論文“Haskell 程式的呼叫模式專業化”中進行了描述。自那篇論文以來,似乎發生了兩個重要的變化:
- SpecConstr 可以應用于同一模塊中的任何呼叫,而不僅僅是單個定義中的遞回呼叫。
- SpecConstr 可以作為引數應用于函式,而不僅僅是建構式。但是,它不適用于 lambda 運算式,除非它們被完全懶惰地浮出水面。
以下是未進行此優化、使用-fno-spec-constr和 并使用-dsuppress-all -dsuppress-uniques -dno-typeable-binds標志生成的核心的相關部分:
Rec {
mapMyList
= \ @ a @ b f ds ->
case ds of {
Nil -> Nil;
Cons a1 rest -> Cons (f a1) (mapMyList f rest)
}
end Rec }
Rec {
main_go
= \ x ->
Cons
(I# x)
(case x of wild {
__DEFAULT -> main_go ( # wild 1#);
5# -> Nil
})
end Rec }
main3 = \ ds -> case ds of { I# x -> I# (*# x 2#) }
main2
= $fShowMyList_$cshowsPrec
$fShowInt $fShowMyList1 (mapMyList main3 (main_go 1#))
main1 = main2 []
main = hPutStr' stdout main1 True
我認為這種優化有點令人失望,因為它僅適用于同一模塊內的使用。此外,很長一段時間(自 2007 年的“Stream Fusion. From Lists to Streams to Nothing at All”論文以來)人們一直希望這種優化可以優化流融合。但是,據我所知,沒有人能夠為此目的使其正常可靠地作業(請參閱此 GHC 問題)。所以現在我們仍然使用foldr/buildbase中的劣質融合。
在這個 GHC 問題中,我開始討論改善現狀的可能性。我認為未來作業中最有希望的方向是改進靜態引數變換優化。請參閱Sebastian Graf 的此評論。
我做了更多的除錯,特別是我使用了該-dverbose-core2core選項并檢查了結果。正如我所料,優化是由于 SpecConstr 優化而發生的。這是 SpecConstr 之前的核心(在 Simplifier pass 之后):
Rec {
mapMyList
= \ @ a @ b f ds ->
case ds of {
Nil -> Nil;
Cons a rest -> Cons (f a) (mapMyList f rest)
}
end Rec }
lvl = \ ds -> case ds of { I# x -> I# (*# x 2#) }
main
= case toList (mapMyList lvl (fromList (eftInt 1# 5#))) of {
...
這是 SpecConstr 之后的核心:
Rec {
$smapMyList
= \ sc ->
case sc of {
Nil -> Nil;
Cons a rest -> Cons (lvl a) (mapMyList lvl rest)
}
mapMyList
= \ @ a @ b f ds ->
case ds of {
Nil -> Nil;
Cons a rest -> Cons (f a) (mapMyList f rest)
}
end Rec }
lvl = \ ds -> case ds of { I# x -> I# (*# x 2#) }
main
= case toList (mapMyList lvl (fromList (eftInt 1# 5#))) of {
...
所以,你可以看到 SpecConstr 創建了那個$smapMyList函式,它是一個mapMyList專用于lvl引數的版本,也就是*2函式。
請注意,尚未使用此專用函式,這是使用新創建的重寫規則完成的,該規則隨后在 Simplifier 運行時觸發。如果您使用該標志,-ddump-rule-rewrites您可以看到它們的作用:
Rule fired
Rule: SC:mapMyList0
Module: (Main)
Before: mapMyList TyArg Int TyArg Int ValArg lvl ValArg rest
After: (\ sc -> $smapMyList sc) rest
Cont: Stop[BoringCtxt] MyList Int
Rule fired
Rule: SC:mapMyList0
Module: (Main)
Before: mapMyList
TyArg Int TyArg Int ValArg lvl ValArg fromList (eftInt 1# 5#)
After: (\ sc -> $smapMyList sc) (fromList (eftInt 1# 5#))
Cont: StrictArg toList
Select nodup wild
Stop[RhsCtxt] String
這是后續 Simplifier pass 之后的核心(它還行內了lvl):
Rec {
$smapMyList
= \ sc ->
case sc of {
Nil -> Nil;
Cons a rest ->
Cons (case a of { I# x -> I# (*# x 2#) }) ($smapMyList rest)
}
end Rec }
main
= case toList ($smapMyList (fromList (eftInt 1# 5#))) of {
...
這也說明了為什么這種優化也需要完全惰性的原因:該lvl函式需要浮出,因為 SpecConstr 不適用于 lambdas。這種浮出需要完全的懶惰。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/365071.html
上一篇:如何列出配對串列的所有可能方法?
