2

У меня есть простой пример игрушек, который, похоже, не согласен с сборщиком мусора о том, какие структуры данных могут быть восстановлены (иначе утечка памяти). Я не пытаюсь придумать более эффективные с точки зрения памяти версии этого алгоритма (здесь есть хорошая коллекция лучших алгоритмов: Haskell Wiki - Prime numbers, а скорее объяснение, почему сборщик мусора не идентифицирует старые, вне области видимости и неиспользуемые части списка, чтобы вернуть эту памятьHaskell сборщик мусора

код здесь:.

import Data.List (foldl') 

erat' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)] 
erat' (c,b) ((x,y):xs) 
    | c < x = (x,y) : erat' (c,b) xs 
    | c == x = (x+y,y) : erat' (c,True) xs 
    | c > x = (x+y,y) : erat' (c,b) xs 
erat' (c,b) [] 
    | b = [] 
    | otherwise = [(c,c)] 

erat :: [Integer] -> [(Integer,Integer)] 
erat = foldl' (\a c -> erat' (c,False) a) [] 

primes :: Integer -> [Integer] 
primes n = map snd $ erat [2..n] 

по сути, вызов простых чисел с положительным целым числом будет возвращать список всех простых чисел до и включая этот номер списка пар простых чисел. и их кратная водяная метка передается в erat 'вместе с парой, включающей кандидата и логическое значение (False for prime и True для non-prime). Каждый нерекурсивный вызов эпохе t 'передаст новый список, и я ожидаю, что вывод будет содержать, в лучшем случае, определенные общие ячейки с самого начала списка до момента первого изменения.

Как только измененные ячейки в списке, переданные в erat, выходят из области видимости, память должна быть отмечена как подлежащая восстановлению, но, как вы можете видеть, когда вы пытаетесь вызвать простые числа с достаточно большим числом (1 000 000, для пример), использование памяти может быстро увеличиться до десятков гигабайт.

Теперь возникает вопрос: почему это происходит? Должен ли сборщик мусора генерации обнаруживать разыменованные ячейки списка, чтобы вернуть их? И не должно быть достаточно легко обнаружить, что у них нет ссылок, потому что:

a) Ничто не может иметь ссылки на структуры данных, более старые, чем они сами; b) не может быть более новых ссылок, потому что эти ячейки/фрагменты больше не являются частью ссылочной структуры данных, так как она выходит из области видимости?

Конечно, измененная структура данных позаботится об этом, но я чувствую, что прибегая к изменчивости в таком случае, это снижает некоторые теоретические принципы для Haskell на полу.

Благодаря людям, которые прокомментировали (в частности, Carl), я немного модифицировал алгоритм, чтобы добавить строгость (и оптимизацию начала пересечения квадрата нового штриха, поскольку более низкие кратные будут пересекаться также кратными младшими числами) ,

Это новая версия:

потребление
import Data.List (foldl') 

erat' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)] 
erat' (c,b) ((x,y):xs) 
    | c < x = x `seq` (x,y) : erat' (c,b) xs 
    | c == x = x `seq` (x+y,y) : erat' (c,True) xs 
    | c > x = x `seq` (x+y,y) : erat' (c,b) xs 
erat' (c,b) [] 
    | b = [] 
    | otherwise = [(c*c,c)] -- lower multiples would be covered by multiples of lower primes 

erat :: [Integer] -> [(Integer,Integer)] 
erat = foldl' (\a c -> erat' (c,False) a) [] 

primes :: Integer -> [Integer] 
primes n = map snd $ erat [2..n] 

память, кажется, все еще быть весьма значительным. Существуют ли какие-либо другие изменения в этом алгоритме, которые могли бы помочь уменьшить общее использование памяти?

Поскольку Уилл отметил, что я не предоставил полную статистику, эти цифры для запуска обновленной версии простых чисел, перечисленных чуть выше, с 100000 в качестве параметра:

Statistics

И после применения изменений, которые будут предложены, использование памяти теперь значительно сократилось.Смотрите, например, на пробеге простых чисел для 100000 раз:

Statistics after changes proposed by Will

И наконец, это окончательный код после того, как предложенные изменения были включены:

import Data.List (foldl') 

erat'' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)] 
erat'' (c,b) ((x,y):xs) 
    | c < x = (x, y) : if x==y*y then (if b then xs 
               else xs++[(c*c,c)]) 
            else erat'' (c,b) xs 
    | c == x = (x+y,y) : if x==y*y then   xs 
            else erat'' (c,True) xs 
    | c > x = (x+y,y) : erat'' (c,b) xs 
erat'' (c,True) [] = [] 
erat'' (c,False) [] = [(c*c,c)] 


primes'' :: Integer -> [Integer] 
primes'' n = map snd $ foldl' (\a c -> (if null a then 0 else 
     case last a of (x,y) -> y) `seq` erat'' (c,False) a) [] [2..n] 

И, наконец, бежать за 1000000 иметь чувство производительности в этой новой версии:

enter image description here

+0

Ваш анализ был бы правильным, если бы Haskell был строгим языком, но это не так. – Carl

+0

Я попытался изменить строгость, используя seq и deepseq, с аналогичными результатами. Я не думаю, что у нестричности есть игра здесь, но я могу ошибаться. Не могли бы вы уточнить? – fgv

+0

Это кажется очень плохим использованием для 'foldl'' на первый взгляд. Вы попробовали 'foldr'? –

ответ

1

нарушение обновление: Вы должны использовать

map snd $ foldl' (\a c -> (if null a then 0 else 
     case last a of (x,y) -> y) `seq` erat' (c,False) a) [] [2..n] 

полностью заставить список на каждой итерации. Это будет consume less memory and run faster.

Причина выше является то, что foldl' только заставляет аккумулятор к слабой головной нормальной форме, и даже при использовании last a достаточно не так, как он будет вынужден просто пара (_,_), не заставляя его составные части.

Но если ваша функция erat' изменена так, что она как можно скорее прекратит просмотр промежуточных списков простых чисел и их кратность и по возможности делится своим хвостом (как описано ниже), это быстрее без принуждения, даже если используя больше памяти.


Ваш (обновлено) код, отредактированы немного для читаемости:

g :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)] 
g (c,b) ((x,y):xs) 
    | c < x = (x, y) : g (c,b) xs -- `c < x` forces the x already, 
    | c == x = (x+y,y) : g (c,True) xs --    no need for `seq` 
    | c > x = (x+y,y) : g (c,b) xs 
g (c,True) [] = [] 
g (c,False) [] = [(c*c,c)] 

primes :: Integer -> [Integer] 
primes n = map snd $ foldl' (\a c -> g (c,False) a) [] [2..n] 

Итак, ваш primes n на самом деле немного, как раз правый на обращенном [2..n] списке. Написание h для flip $ foldl' (\a c -> g (c,False) a), это

= map snd $ h [2..n] $ [] 
= map snd $ h [3..n] $ [(2*2,2)] 
= map snd $ h [4..n] $ (4,2) :(g (3,False) []) 
= map snd $ h [5..n] $ (4+2,2):(g (4,True) $ g (3,False) []) 
= map snd $ h [6..n] $ (6,2) :(g (5,False) $ g (4,True) $ g (3,False) []) 
.... 

Строгость foldl' имеет ограниченное влияние здесь, как аккумулятор вынужден только слабой головной нормальной форме.

Раскладной с (\a c -> last a `seq` g (c,False) a) даст нам

= map snd $ ... $ g (3,False) [(2*2,2)] 
= map snd $ ... $ g (4,False) [(4,2),(3*3,3)] 
= map snd $ ... $ g (5,False) [(4+2,2),(9,3)] 
= map snd $ ... $ g (6,False) [(6,2),(9,3),(5*5,5)] 
= map snd $ ... $ g (7,False) [(6+2,2),(9,3),(25,5)] 
= map snd $ ... $ g (8,False) [(8,2),(9,3),(25,5),(7*7,7)] 
= map snd $ ... $ g (9,False) [(8+2,2),(9,3),(25,5),(49,7)] 
= map snd $ ... $ g (10,False) [(10,2),(9+3,3),(25,5),(49,7)] 
= map snd $ ... $ g (11,False) [(10+2,2),(12,3),(25,5),(49,7)] 
.... 
= map snd $ ... $ g (49,False) 
      [(48+2,2),(48+3,3),(50,5),(49,7),(121,11)...(2209,47)] 
.... 

, но все эти изменения будут протолкнули в список окончательного print в любом случае, так что лень не непосредственная проблема здесь (он вызывает переполнение стека для большого входы, но это вторично здесь). Проблема в том, что ваш erat' (переименованный g выше) в конечном итоге толкает каждую запись по всему списку бесполезно, воссоздает весь список для каждый номер кандидата. Это очень тяжелая модель использования памяти.

Он должен прекратить как можно раньше, и делитесь хвост списка всякий раз, когда возможно:

g :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)] 
g (c,b) ((x,y):xs) 
    | c < x = (x, y) : if x==y*y then (if b then xs 
               else xs++[(c*c,c)]) 
            else g (c,b) xs 
    | c == x = (x+y,y) : if x==y*y then   xs 
            else g (c,True) xs 
    | c > x = (x+y,y) :    g (c,b) xs 
g (c,True) [] = [] 
g (c,False) [] = [(c*c,c)] 

Составлено с -O2 и работать автономно, it runs под ~ N 1,9 против вашей оригинальной функции-х ~ N2.4..2.8..and rising, производящих простых до N.

(конечно, "нормальный" решето Эратосфена должен работать около ~ N 1,1, в идеале, его теоретическая сложность поры до времени N журнал (журнал N)).

+0

Я поменю foldl 'и удалю seq из erat', как вы предложили по уважительным причинам. Однако ранний выход из списка сканирования в erat 'имеет другие последствия. Каждый проход через список для следующего номера обновляет список, пересекая следующий раунд кратных, которые выше кандидата. Выход из раннего времени оставит кратные справа непересекающимися, поэтому следующий номер не будет правильно оценен. Вы сравнили свою версию с моим, чтобы узнать, получили ли вы те же результаты? То, что вы предлагаете, можно сделать с помощью списка приоритетов, предполагая упорядочение кратности, но не этот список. – fgv

+0

@fgv нет, нет, мое редактирование сохраняет семантику. да, конечно, те же результаты. вы * делаете * имеете упорядочение, а не по кратным, но по простым (не по 1-ому, а по 2-ому полю пары). –

+0

btw Я тестировал его только в GHCi, и он дал мне переполнение стека на 100 000, и мне пришлось изменить функцию сгибания, чтобы заставить последний элемент преодолеть это.Я подозреваю, что это также может улучшить ситуацию с памятью. –

3

Предположение а) ложь в присутствии лени. И на самом деле ваш код состоит почти полностью из генерирующих cons-ячеек, на которые указывают более старые ячейки cons. erat' потребляет элемент списка, затем создает конструктор (:), указывающий на кортеж, и не оцененный thunk, который будет выполнять рекурсивный вызов erat'. Только после того, как этот thunk будет оценен позже, конструктор списка (:) фактически укажет на его хвост как структуру данных. Так что да, почти каждый (:), который вы выделяете в erat', фактически указывает вперед во времени. (Единственным исключением является последним - [foo] будет указывать на ранее существовавших [] конструктора, когда его (:) конструктор выделяется.)

Предположение б) нонсенс в присутствии лени. Область определяет видимость в Haskell, а не время жизни. Срок службы зависит от оценки и доступности.

Так что во время выполнения происходит то, что вы строите трубопровод erat' звонков в erat. Каждый из них держится на столько же своих входных данных, сколько и был оценен, медленно потребляя его. Интересная часть заключается в том, что ваш код не оценивает ничего заранее - кажется, что он должен действительно хорошо транслироваться - за исключением того, что конвейер слишком глубокий. Созданный трубопровод составляет приблизительно n этапов - это (неэффективное!) Пробное деление, а не сито Эратосфена. Вы должны только добавлять простые числа к конвейеру, а не каждый номер.

+0

Я попробую добавить строгость, чтобы убедиться, что она ведет себя лучше (раньше казалось, что она вообще не улучшилась). Что касается комментария к тому, что это пробное подразделение, ваша оценка неверна. Числа добавляются только в базовый случай erat ', и только если второй элемент пары является ложным (prime). Список состоит только из простых чисел и их наибольшего числа, вычисленного до сих пор, что и требует стандартная кроватка. – fgv

+0

Добавление строгости делает ее еще хуже, потому что она заставляет тонну больше в память сразу. И это определенно пробное разделение. Если бы это было сито, вы бы никогда не назвали 'erat'' из' foldl'' в 'erat' с c == 4, например. – Carl

+0

erat 'управляет кроваткой, поэтому его нужно вызвать для каждого номера. Однако, с c == 4, erat 'будет иметь список с [(4,2), (3,3)]. В пределах erat '4 будет помечен как не-простой (установка Bool на True), список изменится на [(6,2), (6,3)] (чтобы пересечь следующие кратные 2 и 3) , но 4 (правильно) никогда не будут добавлены в кроватку. С c == 5 список перейдет в [(6,2), (6,3), (5,5)]. За исключением оптимизации, чтобы начать с квадрата нового штриха (так как более низкие кратные уже пересекаются кратными нижними штрихами), вы предлагаете, чтобы детская кроватка работала по-другому? – fgv

Смежные вопросы