Проанализируйте этот фрагмент на Haskell и объясните, что делает код, какие возможности оптимизации предоставляет ленивость и где она может навредить: fibs = 0 : 1 : zipWith (+) fibs (tail fibs) — сколько памяти потребуется для доступа к n-му элементу, и как преобразовать реализацию для контролируемого использования памяти
— это классическое ленивое определение бесконечного списка чисел Фибоначчи. Что тут происходит:
fibs — список, первый элемент 0, второй 1.zipWith +++ fibs tailfibstail fibstailfibs складывает список с его хвостом поэлементно, поэтому получается последовательность сумм предыдущих двух элементов. Благодаря ленивости список самопорождает: новые элементы вычисляются по мере запроса.
Что даёт ленивость плюсыплюсыплюсы
Можно оставить бесконечную структуру и брать сколько нужно streamingstreamingstreaming: fibs !! 10 вычислит только первые 11 элементов.Шэринг: если несколько мест кода читают один и тот же fibs, ранее вычисленные элементы сохраняются и повторно не пересчитываются — экономия времени.Композиция: удобно конструировать конвейеры вычислений без явного управления памятью.
Где ленивость мешает минусы/подводныекамниминусы / подводные камниминусы/подводныекамни
Удержание префикса списка spaceleakspace leakspaceleak. Если у вас сохраняется ссылка на голову fibs например,fibs—top−levelCAFнапример, fibs — top-level CAFнапример,fibs—top−levelCAF, то все уже построенные элементы остаются в памяти. При обращении к n-му элементу fib!!nfib !! nfib!!n будет построена и сохранена вся префиксная часть длины ≈ n — Onnn ячеек памяти.Накопление отложенных вычислений thunksthunksthunks. Если при наращивании списка значения не принудительно не сворачиваются в нормальное представление, можно накопить большую цепочку thunk'ов и получить рост памяти / стековые проблемы.Размер самих чисел. Для типа Integer числа растут экспоненциально по длине в битах, так что и память на отдельный элемент может быть большой — это отдельный фактор.
Сколько памяти требуется для доступа к n-му элементу
В типичном случае при использовании вышеописного top-level fibs и взятии элемента fibs !! n вы аллоцируете примерно n конс-узлов каждыйузелсодержитссылкуназначениеиссылкунаследующийузелкаждый узел содержит ссылку на значение и ссылку на следующий узелкаждыйузелсодержитссылкуназначениеиссылкунаследующийузел плюс сами значения Integer/IntInteger/IntInteger/Int. Итого память ~ Onnnспайнсписка+значенияспайн списка + значенияспайнсписка+значения. Для Integer добавляется память на bigint растётсnрастёт с nрастётсn.Если вы не сохраняете ссылку на голову списка и генерация списка используется только для одного последовательного прохода, GC может освободить уже пройденные узлы; тогда максимальная занимаемая память может быть O111потомучтостарыеузлыуженезадействованыпотому что старые узлы уже не задействованыпотомучтостарыеузлыуженезадействованы. Но при top-level fibs префикс обычно удерживается — значит Onnn.
Как получить контролируемое использование памяти
Если вам нужен только n-й элемент и вы не хотите хранить весь префикс, лучше использовать строгий итеративный алгоритм константнаяпамятьподлинепрефиксаконстантная память по длине префиксаконстантнаяпамятьподлинепрефикса или строгую свёртку. Примеры.
1) Строгая хвостовая рекурсия O(n)времени,O(1)памятиO(n) времени, O(1) памятиO(n)времени,O(1)памяти:
{-# LANGUAGE BangPatterns #-} fib :: Int -> Integer fib n = go n 0 1 where go 0 !a _ = a go k !a !b = go k−1k-1k−1 b a+ba + ba+b
!−patternsгарантируют,чтоаккумуляторынебудутоставатьсяввидеthunk′ов.!-patterns гарантируют, что аккумуляторы не будут оставаться в виде thunk'ов.!−patternsгарантируют,чтоаккумуляторынебудутоставатьсяввидеthunk′ов.
Этот вариант тоже O(n) времени, не накапливает thunk'ов благодаря foldl', но создаёт список [1..n] — можно заменить на явный цикл или использовать специализированные итераторы.
3) Если нужна ленивость + ограниченное кеширование, можно явным образом кэшировать только окно последних нескольких значений или хранить значения в массиве/vector и при необходимости освобождать старые элементы. Например, построить вектор длины n+1 и затем индексировать:
import qualified Data.Vector.Unboxed as VU buildFibVec :: Int -> VU.Vector Integer buildFibVec n = VU.create $ do v <- VU.unsafeNew n+1n+1n+1
VU.unsafeWrite v 0 0 when (n >= 1) VU.unsafeWritev11forM[2..n] VU.unsafeWrite v 1 1 forM_ [2..n] VU.unsafeWritev11forM[2..n] \i -> do a <- VU.unsafeRead v i−2i-2i−2
b <- VU.unsafeRead v i−1i-1i−1
VU.unsafeWrite v i a+ba + ba+b
return v
Этот подход использует Onnn памяти, но она предсказуема и вы освобождаете вектор по завершении.
4) Если вы хотите сохранить ленивый infinite list, но избежать удержания старых элементов, не храните его в top-level CAF. Генерируйте список локально в месте использования и потребляйте его «стримингообразно» без сохранения ссылки на голову; тогда GC сможет освобождать пройденное. Но это ненадёжно и зависит от того, как именно используется список.
Резюме / рекомендации
Определение fibs = 0:1:zipWith +++ fibs tailfibstail fibstailfibs — удобно, лениво и с шеринго́м; хорошо, если вы хотите кешировать префикс и многократно читать одни и те же элементы.Если нужно контролируемое, предсказуемое использование памяти при доступе к n-му элементу, лучше использовать строгую хвостовую рекурсию или строгие свёртки foldl′foldl'foldl′ — это даёт Onnn время и O111 доп. памяти по длине префикса.Следите за типами IntegervsIntInteger vs IntIntegervsInt и за тем, чтобы не накапливались thunk'и — используйте seq или BangPatterns или Data.Strict.Tuple, scanl' и т.п., когда нужно избежать отложенных вычислений.
Если хотите, могу привести конкретные варианты кода сBangPatterns,сscanl′,сVectorс BangPatterns, с scanl', с VectorсBangPatterns,сscanl′,сVector и измерения потребления памяти для разных n.
Код
fibs = 0 : 1 : zipWith +++ fibs tailfibstail fibstailfibs
— это классическое ленивое определение бесконечного списка чисел Фибоначчи. Что тут происходит:
fibs — список, первый элемент 0, второй 1.zipWith +++ fibs tailfibstail fibstailfibs складывает список с его хвостом поэлементно, поэтому получается последовательность сумм предыдущих двух элементов. Благодаря ленивости список самопорождает: новые элементы вычисляются по мере запроса.Что даёт ленивость плюсыплюсыплюсы
Можно оставить бесконечную структуру и брать сколько нужно streamingstreamingstreaming: fibs !! 10 вычислит только первые 11 элементов.Шэринг: если несколько мест кода читают один и тот же fibs, ранее вычисленные элементы сохраняются и повторно не пересчитываются — экономия времени.Композиция: удобно конструировать конвейеры вычислений без явного управления памятью.Где ленивость мешает минусы/подводныекамниминусы / подводные камниминусы/подводныекамни
Удержание префикса списка spaceleakspace leakspaceleak. Если у вас сохраняется ссылка на голову fibs например,fibs—top−levelCAFнапример, fibs — top-level CAFнапример,fibs—top−levelCAF, то все уже построенные элементы остаются в памяти. При обращении к n-му элементу fib!!nfib !! nfib!!n будет построена и сохранена вся префиксная часть длины ≈ n — Onnn ячеек памяти.Накопление отложенных вычислений thunksthunksthunks. Если при наращивании списка значения не принудительно не сворачиваются в нормальное представление, можно накопить большую цепочку thunk'ов и получить рост памяти / стековые проблемы.Размер самих чисел. Для типа Integer числа растут экспоненциально по длине в битах, так что и память на отдельный элемент может быть большой — это отдельный фактор.Сколько памяти требуется для доступа к n-му элементу
В типичном случае при использовании вышеописного top-level fibs и взятии элемента fibs !! n вы аллоцируете примерно n конс-узлов каждыйузелсодержитссылкуназначениеиссылкунаследующийузелкаждый узел содержит ссылку на значение и ссылку на следующий узелкаждыйузелсодержитссылкуназначениеиссылкунаследующийузел плюс сами значения Integer/IntInteger/IntInteger/Int. Итого память ~ Onnn спайнсписка+значенияспайн списка + значенияспайнсписка+значения. Для Integer добавляется память на bigint растётсnрастёт с nрастётсn.Если вы не сохраняете ссылку на голову списка и генерация списка используется только для одного последовательного прохода, GC может освободить уже пройденные узлы; тогда максимальная занимаемая память может быть O111 потомучтостарыеузлыуженезадействованыпотому что старые узлы уже не задействованыпотомучтостарыеузлыуженезадействованы. Но при top-level fibs префикс обычно удерживается — значит Onnn.Как получить контролируемое использование памяти
Если вам нужен только n-й элемент и вы не хотите хранить весь префикс, лучше использовать строгий итеративный алгоритм константнаяпамятьподлинепрефиксаконстантная память по длине префиксаконстантнаяпамятьподлинепрефикса или строгую свёртку. Примеры.
1) Строгая хвостовая рекурсия O(n)времени,O(1)памятиO(n) времени, O(1) памятиO(n)времени,O(1)памяти:
{-# LANGUAGE BangPatterns #-}
fib :: Int -> Integer
fib n = go n 0 1
where
go 0 !a _ = a
go k !a !b = go k−1k-1k−1 b a+ba + ba+b
!−patternsгарантируют,чтоаккумуляторынебудутоставатьсяввидеthunk′ов.!-patterns гарантируют, что аккумуляторы не будут оставаться в виде thunk'ов.!−patternsгарантируют,чтоаккумуляторынебудутоставатьсяввидеthunk′ов.
2) foldl' вариант:
import Data.List foldl′foldl'foldl′
fib n = fst $ foldl' ((a,b) _ -> (b, a+b)) (0,1) [1..n]
Этот вариант тоже O(n) времени, не накапливает thunk'ов благодаря foldl', но создаёт список [1..n] — можно заменить на явный цикл или использовать специализированные итераторы.
3) Если нужна ленивость + ограниченное кеширование, можно явным образом кэшировать только окно последних нескольких значений или хранить значения в массиве/vector и при необходимости освобождать старые элементы. Например, построить вектор длины n+1 и затем индексировать:
import qualified Data.Vector.Unboxed as VU
buildFibVec :: Int -> VU.Vector Integer
buildFibVec n = VU.create $ do
v <- VU.unsafeNew n+1n+1n+1 VU.unsafeWrite v 0 0
when (n >= 1) VU.unsafeWritev11forM[2..n] VU.unsafeWrite v 1 1
forM_ [2..n] VU.unsafeWritev11forM[ 2..n] \i -> do
a <- VU.unsafeRead v i−2i-2i−2 b <- VU.unsafeRead v i−1i-1i−1 VU.unsafeWrite v i a+ba + ba+b return v
Этот подход использует Onnn памяти, но она предсказуема и вы освобождаете вектор по завершении.
4) Если вы хотите сохранить ленивый infinite list, но избежать удержания старых элементов, не храните его в top-level CAF. Генерируйте список локально в месте использования и потребляйте его «стримингообразно» без сохранения ссылки на голову; тогда GC сможет освобождать пройденное. Но это ненадёжно и зависит от того, как именно используется список.
Резюме / рекомендации
Определение fibs = 0:1:zipWith +++ fibs tailfibstail fibstailfibs — удобно, лениво и с шеринго́м; хорошо, если вы хотите кешировать префикс и многократно читать одни и те же элементы.Если нужно контролируемое, предсказуемое использование памяти при доступе к n-му элементу, лучше использовать строгую хвостовую рекурсию или строгие свёртки foldl′foldl'foldl′ — это даёт Onnn время и O111 доп. памяти по длине префикса.Следите за типами IntegervsIntInteger vs IntIntegervsInt и за тем, чтобы не накапливались thunk'и — используйте seq или BangPatterns или Data.Strict.Tuple, scanl' и т.п., когда нужно избежать отложенных вычислений.Если хотите, могу привести конкретные варианты кода сBangPatterns,сscanl′,сVectorс BangPatterns, с scanl', с VectorсBangPatterns,сscanl′,сVector и измерения потребления памяти для разных n.