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