简体   繁体   English

Haskell显式递归与`iterate`

[英]Haskell explicit recursion vs `iterate`

While writing a function using iterate in Haskell, I found that an equivalent version with explicit recursion seemed noticeably faster - even though I believed that explicit recursion ought to be frowned upon in Haskell. 在Haskell中使用iterate编写函数时,我发现具有显式递归的等效版本似乎明显更快 - 尽管我认为在Haskell中应该不赞成显式递归。

Similarly, I expected GHC to be able to inline/optimise list combinators appropriately so that the resulting machine code is at least similarly performing to the explicit recursion. 类似地,我期望GHC能够适当地内联/优化列表组合器,以便得到的机器代码至少类似地执行显式递归。

Here's a (different) example, which also displays the slowdown I observed. 这是一个(不同的)示例,它还显示了我观察到的减速情况。

steps mn and its variant steps' compute the number of Collatz steps n takes to reach 1, giving up after m attempts. steps mn及其变体steps'计算达到1所需的Collat​​z步n ,在m尝试后放弃。

steps uses explicit recursion while steps' uses list functions. 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]

I tested these by evaluating for all values up to 10^7 , each giving up after 800 steps. 我通过评估高达10^7所有值来测试这些值,每个值在800步后放弃。 On my machine (compiled with ghc -O2 ), explicit recursion took just under 4 seconds ( 3.899s ) but list combinators took about 5 times longer ( 19.922s ). 在我的机器上(使用ghc -O2编译),显式递归只花了不到4秒( 3.899s )但列表组合器花了大约5倍( 19.922s )。

Why is explicit recursion so much better in this case, and is there a way of writing this without explicit recursion while preserving performance? 在这种情况下,为什么显式递归会更好,并且有没有一种方法可以在保持性能的同时编写没有显式递归的方法?

Updated: I submitted Trac 15426 for this bug. 更新:我为此错误提交了Trac 15426

The problem disappears if you copy the definitions of elemIndex and findIndex into your module: 如果将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]

The problem seems to be that these must be inlinable for GHC to get the fusion right. 问题似乎是GHC必须能够使这些融合得当。 Unfortunately, neither of them is marked inlinable in Data.OldList . 不幸的是,它们都没有在Data.OldList标记为Data.OldList

The change to allow findIndex to participate in fusion is relatively recent (see Trac 14387 ) where listToMaybe was reimplemented as a foldr . 允许findIndex参与融合的更改是相对较新的(参见Trac listToMaybe ),其中listToMaybe重新实现为foldr So, it probably hasn't seen a lot of testing yet. 所以,它可能还没有看到很多测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM