При написании функции, использующей 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
).
Почему в этом случае явная рекурсия намного лучше, и есть ли способ записать это без явной рекурсии при сохранении производительности?