在Haskell中使用iterate
编写函数时,我发现具有显式递归的等效版本似乎明显更快-尽管我认为在Haskell中不应使用显式递归。
类似地,我希望GHC能够适当地内联/优化列表组合器,以使生成的机器代码至少与显式递归相似。
这是一个(不同的)示例,它也显示了我观察到的速度下降。
steps m n
及其变体steps'
计算n
达到1所需的Collatz步骤数,经过m
次尝试后放弃。
steps
使用显式递归,而steps'
使用列表函数。
import Data.List (elemIndex)
import Control.Exception (evaluate)
import Control.DeepSeq (rnf)
collatz :: Int -> Int
collatz n
| even n = n `quot` 2
| otherwise = 3 * n + 1
steps :: Int -> Int -> Maybe Int
steps m = go 0
where go k n
| n == 1 = Just k
| k == m = Nothing
| otherwise = go (k+1) (collatz n)
steps' :: Int -> Int -> Maybe Int
steps' m = elemIndex 1 . take m . iterate collatz
main :: IO ()
main = evaluate $ rnf $ map (steps 800) $ [1..10^7]
我通过评估直到10^7
的所有值来测试了这些值,每个值在800
步骤之后都放弃了。在我的机器上(用ghc -O2
编译),显式递归花费了不到4秒(3.899s
),但是列表组合器花费了大约5倍(19.922s
)。
为什么在这种情况下显式递归如此好,并且在保持性能的情况下有没有写方法而无需显式递归的方法呢?
答案 0 :(得分:9)
已更新:我为此错误提交了Trac 15426。
如果将elemIndex
和findIndex
的定义复制到模块中,问题将消失:
import Control.Exception (evaluate)
import Control.DeepSeq (rnf)
import Data.Maybe (listToMaybe)
import Data.List (findIndices)
elemIndex :: Eq a => a -> [a] -> Maybe Int
elemIndex x = findIndex (x==)
findIndex :: (a -> Bool) -> [a] -> Maybe Int
findIndex p = listToMaybe . findIndices p
collatz :: Int -> Int
collatz n
| even n = n `quot` 2
| otherwise = 3 * n + 1
steps' :: Int -> Int -> Maybe Int
steps' m = elemIndex 1 . take m . iterate collatz
main :: IO ()
main = evaluate $ rnf $ map (steps' 800) $ [1..10^7]
问题似乎是,因为对于GHC来说,这些必须是不可侵犯的才能正确融合。不幸的是,它们在Data.OldList
中均未标记为不可入侵。
允许findIndex
参与融合的更改是相对较新的(请参阅Trac 14387),其中listToMaybe
被重新实现为foldr
。因此,它可能尚未进行大量测试。