我正在学习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)
Run Code Online (Sandbox Code Playgroud)
我有一种预感,第一个版本做了更多工作,因为它将每个元素放入一个列表并连接到现有sequence,而第二个版本将一个元素添加到列表中.这是性能差异的主要原因还是有其他重要因素,还是我的预感略有偏差?
:set +s在我的机器上显示ghci中的显示(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)
Run Code Online (Sandbox Code Playgroud)
我的第二个预感是,take 10 (eft 1 10000000)应该表现得更好,take 10 (eft' 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
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
至于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)
Run Code Online (Sandbox Code Playgroud)
如果您进行手工评估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.
Run Code Online (Sandbox Code Playgroud)
但至少,一旦你超过了gos,列表就可以消费了.情况并非如此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])
Run Code Online (Sandbox Code Playgroud)
它变得更糟:你必须再次使用列表的剩余尾部为每个元素做!
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.
Run Code Online (Sandbox Code Playgroud)
事实上,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)
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
为了完整起见,这里有相应的手工评估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 : []
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
93 次 |
| 最近记录: |