Performance 生成数字除数的两个简单代码。为什么递归的速度更快?

Performance 生成数字除数的两个简单代码。为什么递归的速度更快?,performance,haskell,recursion,higher-order-functions,factors,Performance,Haskell,Recursion,Higher Order Functions,Factors,在解决一个问题时,我必须计算一个数字的除数。我有两个实现,它们为给定的数字生成所有大于1的除数 第一种是使用简单的递归: divisors :: Int64 -> [Int64] divisors k = divisors' 2 k where divisors' n k | n*n > k = [k] | n*n == k = [n, k] | k `mod` n == 0 = (n:(k `div

在解决一个问题时,我必须计算一个数字的除数。我有两个实现,它们为给定的数字生成所有大于1的除数

第一种是使用简单的递归:

divisors :: Int64 -> [Int64]
divisors k = divisors' 2 k
  where
    divisors' n k | n*n > k = [k]
                  | n*n == k = [n, k]
                  | k `mod` n == 0 = (n:(k `div` n):result)
                  | otherwise = result
      where result = divisors' (n+1) k
第二个使用Prelude中的列表处理函数:

divisors2 :: Int64 -> [Int64]
divisors2 k = k : (concatMap (\x -> [x, k `div` x]) $!
                  filter (\x -> k `mod` x == 0) $! 
                  takeWhile (\x -> x*x <= k) [2..])
使用带延迟求值($)的除数2:

$ ghc --make -O2 div.hs 
[1 of 1] Compiling Main             ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1

real    0m7.461s
user    0m7.444s
sys 0m0.012s
使用函数除数

$ ghc --make -O2 div.hs 
[1 of 1] Compiling Main             ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1

real    0m7.058s
user    0m7.036s
sys 0m0.020s

既然你问了,为了让它更快,应该使用不同的算法。简单而直接的方法是先找到一个素因子分解,然后用它来构造因子

标准是:

factorize::Integral a=>a->[a]
因式分解n=gon(2:[3,5..])--或:`gon素数`
哪里
go n ds@(d:t)
|d*d>n=[n]
|r==0=d:go q ds
|否则=去
式中(q,r)=quotRem n d
--因式分解12348==>[2,2,3,3,7,7]
可以对相等的素因子进行分组和计数:

导入数据列表(组)
primePowers::积分a=>a->[(a,Int)]
素数幂n=[(头x,长度x)|x[(2,2)、(3,2)、(7,3)]
除数通常是通过以下方式构造的,尽管顺序不正确:

除数::积分a=>a->[a]
除数n=映射乘积$序列
[take(k+1)$iterate(p*)1 |(p,k)a->Int
numDivisors n=product[k+1 |(u,k)[ma]>m[a]
对于列表单子
m~[]
构造由一个成员从每个成员列表中选择的所有可能元素组合的列表,
sequence_list=foldr(\xs rs->[x:r | x[a]
ordDivisors n=foldr(\(p,k)->foldi merge[]。取(k+1)。迭代(映射(p*))
[1] $reverse$primen
foldi::(a->a->a)->a->[a]->a
foldi fz(x:xs)=fx(foldi fz(对xs)),其中
成对(x:y:xs)=f x y:pairs xs
对xs=xs
foldi f z[]=z
合并::Ord a=>[a]->[a]->[a]
合并(x:xs)(y:ys)=案例(比较y x)
LT->y:merge(x:xs)ys
_->x:merge xs(y:ys)
合并xs[]=xs
合并[]ys=ys
{ORD除数12348==>
[1,2,3,4,6,7,9,12,14,18,21,28,36,42,49,63,84,98,126,147,196,252,294,343,441,588,
686,882,1029,1372,1764,2058,3087,4116,6174,12348] -}
这个定义也是有效的,即它立即开始产生除数,没有明显的延迟:

{-take 20$ordDivisors$product$concat$replicate 5$take 11素数
==> [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
(0.00秒,525068字节)
numDivisors$产品$concat$复制5$取11个素数
==> 362797056  -}

递归版本通常不会比基于列表的版本快。这是因为当计算遵循特定模式时,GHC编译器会进行优化。这意味着列表生成器和“列表转换器”可能会融合到一个大生成器中

但是,当您使用
$!
时,基本上是告诉编译器“请在执行下一步之前生成此列表的第一个cons”。这意味着GHC必须至少计算一个中间列表元素,这将完全禁用整个融合优化


因此,第二种算法的速度较慢,因为您生成的中间列表必须进行构造和销毁,而递归算法只是直接生成一个列表。

我认为这与Haskell优化了尾部调用有关,因此递归没有其他语言那么受欢迎。但我不这么认为“我不知道这到底有什么帮助,所以我把它作为一个注释而不是答案。不,尾部调用优化通常不会影响Haskell程序的性能,而且Haskell中的编译器很少使用尾部调用优化,因为它是一种延迟求值的语言,大多数时候不需要它。@Dflestr as”我说-我不确定。谢谢你纠正我:)(误读了你的代码…)尝试用
$!
替换
$!
,第二个问题的罪魁祸首可能是过于严格。差别到底有多大?5%?1%?应该使用更好的算法。首先生成素数分解,然后从中构造除数。如果您感兴趣,请在一个新问题中提出此问题。:)谢谢您的回答,但这是最好的答案eems只是解释的一部分,因为似乎仍然存在显著的性能差异。但我现在了解了列表融合!请查看我问题中的编辑。我将在5分钟后再次使用Haskell编译器更新我的答案。我无法复制您的结果。对我来说,第一个版本运行于3.446s,而第二个版本运行于r0.831s中的uns。我使用的是GHC 7.4.2。注意:您在基准测试中使用了
sum
,这导致了完整列表的融合。这就是为什么第二个版本的算法要快得多的原因。例如,您应该强制使用整个列表,而不是使用
sum
,以获得更好的基准测试。顺便说一句,第一个版本的算法使用纯文本递归,因此没有因为这个原因被融合。你可以用
展开器
之类的东西重写它。@donatello你和dflemstr使用相同的优化标志吗?我在编辑中只使用了-O2。但是,我的ghc版本是7.4.1。是这样吗?
$ ghc --make -O2 div.hs 
[1 of 1] Compiling Main             ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1

real    0m7.058s
user    0m7.036s
sys 0m0.020s