Haskell递归效率

Haskell递归效率,haskell,memory,recursion,Haskell,Memory,Recursion,我正在做一些项目(不是作为家庭作业,只是为了好玩/学习),我正在学习Haskell。其中一个问题是找到起始数在100万以下的最大Collatz序列() 所以不管怎样,我能够做到,我的算法工作正常,在编译时很快得到正确的答案。但是,它使用1000000深度递归 所以我的问题是:我做得对吗?照目前的情况,哈斯克尔的正确做法是什么?我怎样才能使它更快?另外,在内存使用方面,递归实际上是如何在底层实现的?它是如何使用内存的 (剧透警报:如果你想独自解决Project Euler的问题14而不看答案,就不

我正在做一些项目(不是作为家庭作业,只是为了好玩/学习),我正在学习Haskell。其中一个问题是找到起始数在100万以下的最大Collatz序列()

所以不管怎样,我能够做到,我的算法工作正常,在编译时很快得到正确的答案。但是,它使用1000000深度递归

所以我的问题是:我做得对吗?照目前的情况,哈斯克尔的正确做法是什么?我怎样才能使它更快?另外,在内存使用方面,递归实际上是如何在底层实现的?它是如何使用内存的

剧透警报:如果你想独自解决Project Euler的问题14而不看答案,就不要看这个。

--哈斯克尔脚本 --问题:找到小于200万的最长collatz链

collatzLength x| x == 1 = 1
               | otherwise = 1 + collatzLength(nextStep x)


longestChain (num, numLength) bound counter
           | counter >= bound = (num, numLength)
           | otherwise = longestChain (longerOf (num,numLength)
             (counter,   (collatzLength counter)) ) bound (counter + 1)
           --I know this is a messy function, but I was doing this problem just 
           --for myself, so I didn't bother making some utility functions for it.
           --also, I split the big line in half to display on here nicer, would
           --it actually run with this line split?


longerOf (a1,a2) (b1,b2)| a2 > b2 = (a1,a2)
                        | otherwise = (b1,b2)

nextStep n | mod n 2 == 0 = (n `div` 2)
           | otherwise = 3*n + 1

main = print (longestChain (0,0) 1000000 1)
当使用-O2编译时,程序运行时间约为7.5秒

那么,有什么建议吗?我想让程序运行得更快,内存使用更少,我想用一种非常Haskellian(这应该是一个词)的方式


提前谢谢

编辑以回答问题

我做对了吗

几乎,正如评论所说,您构建了一个巨大的
1+(1+(1+…)
——使用一个严格的累加器或者更高级别的函数来为您处理事情。还有其他一些小事情,比如定义一个函数来比较第二个元素,而不是使用
maximumBy(比较snd)
,但这更符合风格

照目前的情况,哈斯克尔的正确做法是什么

这是可以接受的惯用Haskell代码

我怎样才能使它更快

请参见下面的基准。对于Euler性能问题,最常见的答案是:

  • 使用-O2(就像你做的那样)
  • Try-fllvm(GHC NCG次优)
  • 使用worker/wrappers来减少参数,或者在您的情况下,利用累加器
  • 使用fast/unbox类型(可以使用Int代替Integer,如果需要可移植性,则使用Int64等)
  • 当所有值均为正值时,使用
    rem
    代替
    mod
    。对于您的情况,了解或发现
    div
    倾向于编译成比
    quot
    慢的东西也很有用
另外,在内存使用方面,递归实际上是如何在底层实现的? 它是如何使用内存的

这两个问题都非常广泛。完整的答案可能需要解决惰性评估、尾部调用优化、工作者转换、垃圾收集等问题。我建议您随着时间的推移更深入地探索这些答案(或者希望这里有人给出我正在避免的完整答案)

原始后基准数据

原件:

$ ghc -O2 so.hs ; time ./so
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
(837799,525)

real    0m5.971s
user    0m5.940s
sys 0m0.019s
将辅助函数与累加器一起用于
collatzLength

$ ghc -O2 so.hs ; time ./so
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
(837799,525)

real    0m5.617s
user    0m5.590s
sys 0m0.012s
使用
Int
并且不要默认为
Integer
——使用类型签名也更容易阅读

$ ghc -O2 so.hs ; time ./so
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
(837799,525)

real    0m2.937s
user    0m2.932s
sys 0m0.001s
使用
rem
而不是
mod

$ ghc -O2 so.hs ; time ./so
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
(837799,525)

real    0m2.436s
user    0m2.431s
sys 0m0.001s
使用
quotRem
而不是
rem
然后使用
div

$ ghc -O2 so.hs ; time ./so
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
(837799,525)

real    0m1.672s
user    0m1.669s
sys 0m0.002s
这与前面的问题非常相似:

编辑:是的,正如Daniel Fischer所建议的,使用
&.
shiftR
的位运算改进了
quotRem

$ ghc -O2 so.hs ; time ./so
(837799,525)

real    0m0.314s
user    0m0.312s
sys 0m0.001s
或者你可以直接使用LLVM,让它发挥它的魔力(注意,这个版本仍然使用
quotRem
still)

LLVM实际上做得很好,只要您避免了
mod
的隐藏,并且使用
rem
甚至
优化基于防护的代码,与使用
shiftR
手动优化的
&.
一样好

以获得比原始速度快20倍左右的结果

编辑:人们对quotRem在面对
Int
时的性能以及位操作感到惊讶。代码包括在内,但我不清楚令人惊讶的是:仅仅因为某些东西可能是负的,并不意味着你不能用非常相似的位操作来处理它,这些操作在正确的硬件上可能具有相同的成本。所有三个版本的
nextStep
的性能似乎相同(
ghc-O2-fforce recomp-fllvm
、ghc版本7.6.3、LLVM 3.3、x86-64)


我会尝试使
collatzLength
迭代(尾部递归)。现在使用递归
1+(1+(1+…)
构建结果。应用与
longestChain
相同的尾部递归技术。现在使用
n.&。1==0
n`shiftR`1
而不是
quotRem
@DanielFischer我多次读到一个好的编译器应该自己做这样的优化。您知道GHC(可能是LLVM后端)的情况吗?使用显式位移位操作真的有帮助吗?@PetrPudlák GHC还没有学会很多这样的优化方法,很少有人在上面工作,他们的时间都花在更高级别的优化和类型系统技巧上,但它本身并没有做到这一点(到目前为止)。当您使用正确的类型时,LLVM后端会进行优化(上次我检查时,仅针对
quot/rem
,而不是
div/mod
)。但是当然,对于
Int
,它必须考虑到负数的可能性,因此您可以使用只出现正数的领域知识来击败它(不会太多)[除非
Int
是32位,在这种情况下,您有溢出]。至少在我的框中,即使使用LLVM后端,简单的移动就可以击败LLVM优化(惊人的大幅度,~25%),因为LLVM不知道没有出现负数。当使用
Word
@Satvik时,这种差异消失了,它们无法区分。我假设LLVM识别与
quotRem x 2
等价的程序集并重写它。
$ time ./so
(837799,525)

real    0m0.286s
user    0m0.283s
sys 0m0.002s
{-# LANGUAGE BangPatterns, UnboxedTuples #-}

import Data.Bits

collatzLength :: Int -> Int
collatzLength x| x == 1    = 1
               | otherwise = go x 0
 where
    go 1 a  = a + 1
    go x !a = go (nextStep x) (a+1)


longestChain :: (Int, Int) -> Int -> Int -> (Int,Int)
longestChain (num, numLength) bound !counter
   | counter >= bound = (num, numLength)
   | otherwise = longestChain (longerOf (num,numLength) (counter, collatzLength counter)) bound (counter + 1)
           --I know this is a messy function, but I was doing this problem just 
           --for myself, so I didn't bother making some utility functions for it.
           --also, I split the big line in half to display on here nicer, would
           --it actually run with this line split?


longerOf :: (Int,Int) -> (Int,Int) -> (Int,Int)
longerOf (a1,a2) (b1,b2)| a2 > b2 = (a1,a2)
                        | otherwise = (b1,b2)
{-# INLINE longerOf #-}

nextStep :: Int -> Int
-- Version 'bits'
nextStep n = if 0 == n .&. 1 then n `shiftR` 1 else 3*n+1
-- Version 'quotRem'
-- nextStep n = let (q,r) = quotRem n 2 in if r == 0 then q else 3*n+1
-- Version 'almost the original'
-- nextStep n | even n = quot n 2
--            | otherwise  = 3*n + 1
{-# INLINE nextStep #-}


main = print (longestChain (0,0) 1000000 1)