簡體   English   中英

enumFromTo如何在haskell中工作,以及哪些優化加速了GHC實現與天真實現之間的關系

[英]how does enumFromTo work in haskell and what optimizations speed up the GHC implementation vs naive implementations

我正在學習haskell,其中一個練習要求我編寫一個與enumFromTo相當的函數。

我想出了以下兩個實現:

eft' :: Enum a => a -> a -> [a]
eft' x y = go x y []
  where go a b sequence
          | fromEnum b < fromEnum a = sequence
          | otherwise = go (succ a) b (sequence ++ [a])

eft :: Enum a => a -> a -> [a]
eft x y = go x y []
  where go a b sequence
          | fromEnum b < fromEnum a = sequence
          | otherwise = go a (pred b) (b : sequence)

我有一種預感,第一個版本做了更多工作,因為它將每個元素放入一個列表並連接到現有sequence ,而第二個版本將單個元素添加到列表中。 這是性能差異的主要原因還是有其他重要因素,還是我的預感略有偏差?

在ghci中測試:set +s在我的機器上顯示(Windows 10,GHC 8.2.2,intel i7-4770HQ):

*Lists> take 10 (eft 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(9.77 secs, 3,761,292,096 bytes)
*Lists> take 10 (eft' 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(27.97 secs, 12,928,385,280 bytes)
*Lists> take 10 (enumFromTo 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(0.00 secs, 1,287,664 bytes)

我的第二個預感是, take 10 (eft 1 10000000)應該比take 10 (eft' 10000000) take 10 (eft 1 10000000)表現更好,因為后者必須從10000000到10之間建立列表才能返回我們take任何有用的值。 顯然這種預感是錯誤的,我希望有人可以解釋原因。

最后,ghc實現比我的天真實現更高效。 我很想知道已經應用了其他優化來加速它。 這個類似標題為SO問題的答案分享了一些似乎來自ghc實現的代碼,但沒有解釋“骯臟”如何提高效率。

eft的問題在於它仍然需要構建整個列表,無論您是否嘗試使用take 10來減少它。 當你想懶洋洋地構建東西時,尾遞歸不是你的朋友 你想要的是保護遞歸 (即在相關構造函數后面的遞歸調用,如在foldr ,這樣當你不需要它們時它們可以不被評估):

eft'' :: Enum a => a -> a -> [a]
eft'' x y
    | fromEnum y < fromEnum x = []
    | otherwise = x : eft'' (succ x) y
GHCi> take 10 (eft 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(7.48 secs, 2,160,291,096 bytes)
GHCi> take 10 (eft'' 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(0.00 secs, 295,752 bytes)
GHCi> take 10 (enumFromTo 1 10000000)
[1,2,3,4,5,6,7,8,9,10]
(0.00 secs, 293,680 bytes)

至於eft'eft更糟,那確實與(++) 作為參考,這里是take(++) 的定義 (我使用報告定義而不是GHC ,但這里的細微差別實際上並不重要):

take                   :: Int -> [a] -> [a]  
take n _      | n <= 0 =  []  
take _ []              =  []  
take n (x:xs)          =  x : take (n-1) xs 

(++) :: [a] -> [a] -> [a]  
[]     ++ ys = ys  
(x:xs) ++ ys = x : (xs ++ ys)

如果您手工評估eft ,您可以在給出任何元素之前了解如何構建整個列表:

take 3 (eft 1 5)
take 3 (go 1 5 [])
take 3 (go 1 4 (5 : []))
take 3 (go 1 3 (4 : 5 : []))
-- etc.
take 3 (1 : 2 : 3 : 4 : 5 : [])
1 : take 2 (2 : 3 : 4 : 5 : [])
1 : 2 : take 1 (3 : 4 : 5 : [])
-- etc.

但至少,一旦你超越了go s,列表就可以消費了。 對於eft'情況並非如此 - (++)仍然需要處理,並且這樣做與列表的長度成線性關系:

take 3 (eft' 1 5)
take 3 (go 1 5 [])
take 3 (go 2 5 ([] ++ [1]))
take 3 (go 3 5 (([] ++ [1]) ++ [2]))
-- etc.
take 3 ((((([] ++ [1]) ++ [2]) ++ [3]) ++ [4]) ++ [5])
take 3 (((([1] ++ [2]) ++ [3]) ++ [4]) ++ [5])
take 3 ((((1 : ([] ++ [2])) ++ [3]) ++ [4]) ++ [5])
take 3 ((((1 : [2]) ++ [3]) ++ [4]) ++ [5])
take 3 (((1 : ([2] ++ [3])) ++ [4]) ++ [5])
-- etc.
take 3 (1 : ((([2] ++ [3]) ++ [4]) ++ [5]))
1 : take 2 ((([2] ++ [3]) ++ [4]) ++ [5])

它變得更糟:你必須再次使用列表的剩余尾部為每個元素做!

1 : take 2 ((([2] ++ [3]) ++ [4]) ++ [5])
1 : take 2 (((2 : ([] ++ [3])) ++ [4]) ++ [5])
1 : take 2 (((2 : [3]) ++ [4]) ++ [5])
1 : take 2 ((2 : ([3] ++ [4])) ++ [5])
-- etc.
1 : take 2 (2 : (([3] ++ [4]) ++ [5]))
1 : 2 : take 1 (([3] ++ [4]) ++ [5])
-- etc.

事實上, take 10偽裝了這樣一個事實,即eft'eft不同,是二次方的:

GHCi> last $ eft' 1 10000
10000
(1.83 secs, 4,297,217,200 bytes)
GHCi> last $ eft' 1 20000
20000
(7.59 secs, 17,516,804,952 bytes)
GHCi> last $ eft 1 5000000
5000000
(3.81 secs, 1,080,282,784 bytes)
GHCi> last $ eft 1 10000000
10000000
(7.51 secs, 2,160,279,232 bytes)

為了完整起見,這里是eft''的相應手工評估:

take 3 (eft'' 1 5)
take 3 (1 : eft'' 2 5)
1 : take 2 (eft'' 2 5) -- No need to evaluate `eft'' 2 5` to get the first element.
1 : take 2 (2 : eft'' 3 5)
1 : 2 : take 1 (eft'' 3 5)
-- etc.
1 : 2 : 3 : take 0 (eft'' 4 5) -- No need to go further.
1 : 2 : 3 : []

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM