Проанализируйте этот фрагмент на Haskell и объясните, что делает код, какие возможности оптимизации предоставляет ленивость и где она может навредить: fibs = 0 : 1 : zipWith (+) fibs (tail fibs) — сколько памяти потребуется для доступа к n-му элементу, и как преобразовать реализацию для контролируемого использования памяти

30 Сен в 17:27
15 +1
0
Ответы
1

Код

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например,fibstoplevelCAF, то все уже построенные элементы остаются в памяти. При обращении к 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-1k1 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-2i2 b <- VU.unsafeRead v i−1i-1i1 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.

30 Сен в 18:55
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир