Ваша реализация fib2 использует мемоизацию, но каждый раз, когда вы вызываете fib2, она перестраивает «весь» результат. Включите профилирование времени и размера ghci:
Prelude> :set +s
Если бы он делал запоминание «между» вызовами, последующие вызовы были бы быстрее и не использовали бы память. Позвоните на номер 2 20000 и убедитесь сами.
Для сравнения - более идиоматическая версия, в которой вы определяете точную математическую идентичность:
-- the infinite list of all fibs numbers.
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
memoFib n = fibs !! n
на самом деле используют запоминание, как вы видите. Если вы запустите memoFib 20000 дважды, вы увидите время и пространство, занятое в первый раз, тогда второй вызов будет мгновенным и не займет память. Никакая магия и никакие скрытые воспоминания, такие как комментарий, не могли бы намекать.
Теперь о вашем первоначальном вопросе: оптимизация и рассуждения о производительности в Haskell ...
Я бы не назвал себя экспертом в Haskell, я использую его только 3 года, 2 из которых на своем рабочем месте, но мне все же пришлось оптимизировать и понять, как рассуждать о его производительности.
Как уже упоминалось в других публикациях, лень - ваш друг, и он может помочь вам повысить производительность, однако вы должны контролировать то, что лениво оценивается, а что строго оценивается.
Проверьте это сравнение foldl против foldr
foldl на самом деле хранит «как» вычислить значение, то есть оно лениво. В некоторых случаях вы экономите время и пространство, будучи ленивыми, как «бесконечные» выдумки. Бесконечные "выдумки" не генерируют их все, но знают как. Когда вы знаете, что вам понадобится значение, вы можете просто получить его «строго», говоря… Вот где аннотации строгости полезны, чтобы вернуть вам контроль.
Я вспоминаю, что читал много раз, что в lisp вы должны "минимизировать" потребление.
Понимание того, что строго оценивается и как заставить это сделать, важно, но так же важно понимать, сколько «мусора» вы делаете для памяти. Помните, что Haskell является неизменным, это означает, что обновление «переменной» фактически создает копию с модификацией. Добавление с (:) значительно эффективнее, чем с (++), потому что (:) не копирует память в противоположность (++). Всякий раз, когда обновляется большой атомарный блок (даже для одного символа), весь блок должен быть скопирован для представления «обновленной» версии. То, как вы структурируете данные и обновляете их, может сильно повлиять на производительность. Профилировщик GHC ваш друг и поможет вам определить их. Конечно, сборщик мусора работает быстро, но если он не работает, то быстрее!
Приветствия