Яркая рекурсия 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
. Таким образом, он, вероятно, еще не видел много испытаний.