我开始尝试让我的头部围绕着哈斯克尔的表现,以及让事情快速而缓慢的原因,我对此感到有些困惑。
我有一个函数的两个实现,它生成一个特定值的素数列表。第一个是直接离开Haskell wiki:
primesTo :: (Ord a, Num a, Enum a) => a -> [a]
primesTo m = eratos [2..m] where
eratos [] = []
eratos (p:xs) = p : eratos (xs `minus` [p*p, p*p+p..m])
第二个是相同的,但在内部使用无限列表:
primes2 :: (Ord a, Num a, Enum a) => a -> [a]
primes2 m = takeWhile (<= m) (eratos [2..]) where
eratos [] = []
eratos (p:xs) = p : eratos (xs `minus` [p*p, p*p+p..])
在这两种情况下,减号功能是:
minus :: (Ord a) => [a] -> [a] -> [a]
minus (x:xs) (y:ys) = case (compare x y) of
LT -> x : minus xs (y:ys)
EQ -> minus xs ys
GT -> minus (x:xs) ys
minus xs _ = xs
后者的实现比前者慢很多(约100倍),我不明白为什么。我本以为haskell的懒惰评估会让它们在引擎盖下相当等效。
对于问题而言,这显然是一个简化的测试用例 - 在现实生活中,优化没有问题(虽然我不明白为什么需要它),但对我来说只是生成无限列表的函数素数比有限列表更有用,但使用起来似乎较慢。
答案 0 :(得分:6)
在我看来,
之间存在很大差异(xs `minus` [p*p, p*p+p..m]) -- primesTo
(xs `minus` [p*p, p*p+p..]) -- primes2
函数minus
成对逐步遍历列表,并在一个列表到达结尾时终止。在上面的第一个minus
表达式中,当后一个列表用完时,这不会超过(m-p*p)/p
个步骤。在第二个中,它总是按length xs
的顺序执行。
所以你的无限列表至少禁用了一次有意义的优化。
答案 1 :(得分:3)
一个区别是,在第二种情况下,您需要生成一个额外的素数。在m
知道停止时间之前,您需要生成大于takeWhile
的第一个素数。
此外,要过滤的列表和倍数列表上的[..m]
边界有助于减少计算次数。每当其中一个列表变空时minus
立即通过其secons子句返回,而在无限情况下,减号在第一种情况下被卡住。如果你还测试只有一个列表是无限的情况,你可以更好地探索这个:
--this is also slow
primes3 :: (Ord a, Num a, Enum a) => a -> [a]
primes3 m = takeWhile (<= m) (eratos [2..m]) where
eratos [] = []
eratos (p:xs) = p : eratos (xs `minus` [p*p, p*p+p..])
--this fast
primes4 :: (Ord a, Num a, Enum a) => a -> [a]
primes4 m = takeWhile (<= m) (eratos [2..]) where
eratos [] = []
eratos (p:xs) = p : eratos (xs `minus` [p*p, p*p+p..m])