使用无限列表时执行速度较慢

时间:2014-01-19 20:02:29

标签: performance haskell

我开始尝试让我的头部围绕着哈斯克尔的表现,以及让事情快速而缓慢的原因,我对此感到有些困惑。

我有一个函数的两个实现,它生成一个特定值的素数列表。第一个是直接离开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的懒惰评估会让它们在引擎盖下相当等效。

对于问题而言,这显然是一个简化的测试用例 - 在现实生活中,优化没有问题(虽然我不明白为什么需要它),但对我来说只是生成无限列表的函数素数比有限列表更有用,但使用起来似乎较慢。

2 个答案:

答案 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])