Яркая рекурсия Haskell vs 'iterate'

При написании функции, использующей iterate в Haskell, я обнаружил, что эквивалентная версия с явной рекурсией казалась заметно быстрее - хотя я полагал, что явная рекурсия должна быть неодобрительной в Haskell.

Аналогично, я ожидал, что GHC сможет соответствующим образом комбинировать/оптимизировать компиляторы списка, чтобы полученный машинный код, по меньшей мере, аналогичным образом выполнял явную рекурсию.

Здесь (другой) пример, который также показывает замедление, которое я наблюдал.

steps mn и его варианты steps' вычислить количество шагов Collatz n чтобы достичь 1, отказавшись после 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

Обновлено: я отправил 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 необходимо быть уверенным в правильном слиянии. К сожалению, ни один из них не отмечен inlinable в Data.OldList.

Изменение, позволяющее findIndex участвовать в слиянии, относительно недавно (см. Trac 14387), где listToMaybe было переопределено как foldr. Таким образом, он, вероятно, еще не видел много испытаний.