Haskell显式递归与“迭代”

时间:2018-07-19 20:39:20

标签: performance loops haskell ghc compiler-optimization

在Haskell中使用iterate编写函数时,我发现具有显式递归的等效版本似乎明显更快-尽管我认为在Haskell中不应使用显式递归。

类似地,我希望GHC能够适当地内联/优化列表组合器,以使生成的机器代码至少与显式递归相似。

这是一个(不同的)示例,它也显示了我观察到的速度下降。

steps m n及其变体steps'计算n达到1所需的Collat​​z步骤数,经过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)。

为什么在这种情况下显式递归如此好,并且在保持性能的情况下有没有写方法而无需显式递归的方法呢?

1 个答案:

答案 0 :(得分:9)

已更新:我为此错误提交了Trac 15426

如果将elemIndexfindIndex的定义复制到模块中,问题将消失:

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。因此,它可能尚未进行大量测试。