2013-04-14 2 views
27

Я читаю книгу программирования Clojure O'Reilly ..Удержание Clojure Head

Я пришел к примеру удержания головы. Первого примера сохраняет ссылку на d (я полагаю), поэтому он не получает мусор:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count d) (count t)]) 
;= #<OutOfMemoryError java.lang.OutOfMemoryError: Java heap space> 

Хотя второй пример оленьей кожи сохраняет его, поэтому он идет без проблем:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count t) (count d)]) 
;= [12 99999988] 

То, что я дон вот что именно сохраняется в этом случае и почему. Если я пытаюсь вернуться только [(count d)], как это:

(let [[t d] (split-with #(< % 12) (range 1e8))] 
    [(count d)]) 

, кажется, чтобы создать такую ​​же проблему памяти.

Далее я вспоминаю, что count в каждом случае реализует/оценивает последовательность. Итак, мне нужно это уточнить.

Если я попытаюсь вернуть (count t) во-первых, то как это ускорить/повысить эффективность памяти, если я вообще не верну его? А что такое & Почему в этом случае сохраняется?

ответ

25

В первом и последнем примерах исходная последовательность, прошедшая до split-with, сохраняется при полной реализации в памяти; следовательно, OOME. То, как это происходит, является косвенным; то, что удерживается непосредственно, составляет t, а исходная последовательность удерживается t, ленивая секунда, в ее нереализованной состоянии.

Путь t приводит к тому, что первоначальная последовательность должна быть проведена следующим образом. До реализации, t является объектом LazySeq, хранящим thunk, который может быть вызван в какой-то момент для реализации t; этому thunk необходимо сохранить указатель на исходный аргумент последовательности до split-with, прежде чем он будет реализован, чтобы передать его на take-while - см. реализацию split-with. Как только t реализуется, бит имеет право на GC (поле, которое удерживает его в объекте LazySeq, установлено на null) на t больше не удерживает голову огромного ввода.

Входной seq сам реализуется полностью (count d), который должен реализовать d, и, таким образом, исходный входной сигнал.

Переходя почему t в настоящее время сохраняется:

В первом случае, это происходит потому, что (count d) получает оценку до того (count t). Поскольку Clojure оценивает эти выражения слева направо, локальный t должен зависнуть для второго вызова для подсчета, и поскольку он, как правило, удерживается на огромном seq (как объяснялось выше), это приводит к OOME.

Окончательный пример, когда возвращается только (count d), должен в идеале не удержаться до t; причина, по которой это не так, несколько тонкая и лучше всего объясняется ссылкой на второй пример.

Второй пример работает нормально, потому что после оценки (count t)t больше не требуется. Компилятор Clojure замечает это и использует умный трюк, чтобы локальный сброс до nil одновременно с созданным вызовом count. Критическая часть кода Java делает что-то вроде f(t, t=null), так что текущее значение t передается соответствующей функции, но локаль очищается до того, как управление передано в f, так как это происходит как побочный эффект выражения t=null, который является аргументом f; очевидно, что семантика Java слева направо - ключ к этой работе.

Обратно к окончательному примеру это не работает, потому что t фактически не используется в любом месте, а неиспользуемые местные жители не обрабатываются процессом очистки местных жителей. (Очистка происходит в точке последнего использования, при отсутствии такой точки в программе нет клиринга.)

Что касается count, осуществляющих ленивые последовательности: он должен это делать, поскольку нет общего способа предсказывая длину ленивого seq, не осознавая этого.

+0

Вы говорите, что «местный т необходимо повесить на второй вызов, чтобы подсчитать, а так как это происходит, чтобы указать на огромный seq, что приводит к OOME». Но 't' составляет всего 12 пунктов. Как избавиться от 't' играть такую ​​огромную роль, если она составляет 12 элементов. И сохранение' d' в памяти не является проблемой, хотя это все остальное '(диапазон 1e8)' –

+1

Ну, 't 'до того, как он используется каким-то образом, - это нереализованный ленивый seq, который внутренне содержит некоторый код, который в какой-то момент может быть вызван для реализации' t'. Этот код должен содержать указатель на исходную последовательность, переданную в 'split-with', поэтому он может передать ее на' take-while' (см. Реализацию 'split-with'). Спасибо за комментарий - это должно быть частью ответа, я отредактирую его. –

+0

Итак, что вы говорите, это то, что 't' удерживается, он фактически содержит целую исходную последовательность в памяти? Итак, если вычисляется '(count d)', параллельно с которым мы имеем clojure, сохраняя всю исходную последовательность, реализованную в памяти? –

22

Ответа на этот вопрос @ Michał Marczyk, если вы считаете правильным, немного сложно понять. Я считаю, что this post on Google Groups легче понять.

Вот как я это понимаю:

Шаг 1 Создать ленивую последовательность: (range 1e8). Значения еще не реализованы, я отметил их asterixes (*):

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 

Шаг 2 Создайте еще два ленивых seqences, которые являются «окном», через которые вы посмотрите на оригинал, огромные ленивые последовательности. Первое окно содержит только 12 элементов (t), другой остальной части элементов (d):

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 

Шаг 3 - выход из сценария памяти - вы вычислите [(count d) (count t)]. Итак, сначала вы подсчитываете элементы в d, затем в t. Что будет происходить в том, что вы будете пройти через все значения, начиная с первого элемента d и реализовать их (отмечен как !):

* * * * * * * * * * * * * ! * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^
         start here and move right -> 

* * * * * * * * * * * * * ! ! * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

* * * * * * * * * * * * * ! ! ! * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

        ... 

; this is theoretical end of counting process which will never happen 
; because of OutOfMemoryError 
* * * * * * * * * * * * * ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ... ! ! ! 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
                    ^

Проблема заключается в том, что все реализованные ценности (!) в настоящее время сохраняется, поскольку глава коллекции (первые 12 элементов) по-прежнему необходимы - нам еще нужно оценить (count t). Это расходует много памяти, что приводит к сбою JVM.

Шаг 3 - действительный сценарий - на этот раз вы оцениваете [(count t) (count d)]. Таким образом, мы сначала хотим посчитать элементы в меньшем, головки последовательности:

! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
^ 
start here and move right -> 

         ! * * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^

Затем мы считаем элементы в d последовательности.Компилятор знает, что элементы из t больше не нужны, так что он может собирать мусор их освобождая память:

      ! * * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
         ^

          ! * * * * * * * * * * * * * * * ... * * * 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
          ^

        ... 

                  ...  ! 
t t t t t t t t t t t t t d d d d d d d d d d d d d d d d d ... d d d 
                    ^

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

+2

Большое спасибо за это kamituel. Я понял отличный ответ Михала Марчика, но это делает его более ясным. – Mars

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