Гарантия оптимизации хвоста - циклическое кодирование в Haskell - PullRequest
11 голосов
/ 05 февраля 2012

Итак, краткая версия моего вопроса: как мы должны кодировать циклы в Haskell, в общем ? В Haskell нет гарантии оптимизации хвоста, паттерны взрыва даже не являются частью стандарта (верно?), И парадигма фолда / развёртывания не гарантированно работает во всех ситуациях. Вот рассматриваемый случай , где только шаблоны взрыва помогли мне заставить его работать в постоянном пространстве (даже использование $! помогло ... хотя тестирование было сделано в Ideone.com, который использует ghc-6.8.2).

В основном речь идет о вложенном цикле, который в list-paradigm можно определить как

prod (sum,concat) . unzip $ 
    [ (c, [r | t]) | k<-[0..kmax], j<-[0..jmax], let (c,r,t)=...]
prod (f,g) x = (f.fst $ x, g.snd $ x)

Или в псевдокоде:

let list_store = [] in
for k from 0 to kmax
    for j from 0 to jmax
        if test(k,j) 
            list_store += [entry(k,j)]
        count += local_count(k,j)
result = (count, list_store)

Пока я не добавил к нему паттерн взрыва, у меня был либо выброс памяти, либо даже переполнение стека. Но шаблоны взрыва не являются частью стандарта, верно? Таким образом, вопрос в том, как можно кодировать вышеупомянутое в стандартном Haskell для работы в постоянном пространстве?

Вот код теста . Расчет поддельный, но проблемы те же. РЕДАКТИРОВАТЬ: Код foldr -образный:

testR m n = foldr f (0,[]) 
               [ (c, [(i,j) | (i+j) == d ])
                 | i<- [0..m], j<-[0..n], 
                   let c = if (rem j 3) == 0 then 2 else 1 ]
  where d = m + n - 3
    f (!c1, [])     (!c, h) = (c1+c,h) 
    f (!c1, (x:_))  (!c, h) = (c1+c,x:h)

Попытка запустить print $ testR 1000 1000 приводит к переполнению стека. Изменение на foldl будет успешным только при использовании шаблонов взрыва в f, но оно строит список в обратном порядке. Я хотел бы построить его лениво и в правильном порядке. Можно ли это сделать с помощью любого вида fold для решения idiomatic ?

РЕДАКТИРОВАТЬ: Подводя итог ответа, который я получил от @ehird: нечего бояться, используя шаблон взрыва. Хотя и не в самом стандартном Haskell, он легко кодируется в нем как f ... c ... = case (seq c False) of {True -> undefined; _ -> ...}. Урок заключается в том, что только сопоставление с образцом приводит к значению, и seq делает НЕ принудительно что-либо сам по себе, а скорее организует это , когда seq x y принудительно - с помощью сопоставления с образцом - x тоже будет вынужден, и y будет ответом. Вопреки тому, что я мог понять из онлайн-отчета, $! действительно НЕ заставляет что-либо само по себе, хотя называется «оператором строгого применения» .

И точка из @stephentetley - строгость очень важна для управления пространственным поведением. Таким образом, вполне нормально кодировать циклы в Haskell с надлежащим использованием аннотаций строгости с шаблонами взрыва, где это необходимо, для написания любого вида специальной функции свертывания (то есть, потребления структуры), которая необходима - как я в конечном итоге делал в первую очередь. - и полагаться на GHC для оптимизации кода.

Большое спасибо всем за помощь.

Ответы [ 2 ]

15 голосов
/ 05 февраля 2012

Образцы взрыва - просто сахар для seq - всякий раз, когда вы видите let !x = y in z, это можно перевести в let x = y in x `seq` z.seq является стандартным, поэтому нет проблем с переводом программ, использующих шаблоны взрыва, в переносимую форму.

Это правда, что Haskell не дает никаких гарантий в отношении производительности - в отчете даже не определена оценка порядок (только то, что он не должен быть строгим), не говоря уже о существовании или поведении стека времени выполнения.Однако, хотя в отчете не указан конкретный метод реализации, вы, безусловно, можете оптимизировать его для одного.

Например, вызов по требованию (и, следовательно, совместное использование) используется всеми реализациями Haskell на практике,и имеет жизненно важное значение для оптимизации кода на Haskell для использования памяти и скорости.В самом деле, чистый трюк с запоминанием 1 (так как полагается на совместное использование (без него это просто замедлит работу).

Эта базовая структура позволяет нам увидеть, например, переполнение стекавызваны созданием слишком больших блоков. Так как вы не опубликовали весь свой код, я не могу сказать вам, как переписать его без шаблонов взрыва, но я подозреваю, что [ (c, [r | t]) | ... ] должно стать [ c `seq` r `seq` t `seq` (c, [r | t]) | ... ]. Конечно, взрывшаблоны более удобны, поэтому они являются таким распространенным расширением! (С другой стороны, вам, вероятно, не нужно форсировать всех из них; знание того, что форсировать, полностью зависит от конкретной структурыкода, и дикое добавление паттернов взрыва ко всему обычно просто замедляет ход событий.)

Действительно, "хвостовая рекурсия" per se не так уж много значит в Haskell: если ваш аккумуляторпараметры не являются строгими, вы переполните стек, когда позже попытаетесь их форсировать, и, действительно, благодаря лени, многие non -tail-рекурсивные программы не работаютпереполнить стек;печать repeat 1 никогда не будет переполнять стек, хотя определение - repeat x = x : repeat x - явно имеет рекурсию в нехвостовой позиции.Это потому, что (:) ленив во втором аргументе;если вы пройдете по списку, у вас будет постоянное использование пространства, так как форсируются repeat x thunks, а предыдущие cons-ячейки выбрасываются сборщиком мусора.

На более философском замечании tail-рекурсивные циклы обычно считаются субоптимальными в Хаскеле.В общем, вместо того, чтобы итеративно вычислять результат по шагам, мы предпочитаем сгенерировать структуру со всеми шаговыми эквивалентами на листьях и выполнить преобразование (например, складку) для получения окончательного результата.,Это гораздо более высокоуровневое представление о вещах, которое становится эффективным благодаря лени (структура создается и собирается как мусор по мере обработки, а не сразу). 2

Этосначала может потребоваться некоторое привыкание, и это, конечно, не работает во всех случаях - чрезвычайно сложные структуры циклов могут быть проблемой для эффективного перевода 3 - но прямой перевод хвост-рекурсивных циклов в Haskell может бытьбольно именно потому, что на самом деле это не так уж идиоматично.

Что касается вставки, на которую вы ссылаетесь, id $! x не работает, чтобы принудить что-либо, потому что это то же самое, что x `seq` id x, который являетсятакой же как x `seq` x, который совпадает с x.По сути, всякий раз, когда форсируется x `seq` y, форсируется x, и в результате получается y.Вы не можете использовать seq, чтобы просто заставить вещи в произвольных точках;вы используете его, чтобы принудительно настроить * thunks в зависимости от других thunks.

В этом случае проблема в том, что вы создаете большой thunk в c, поэтому вывероятно, хотите заставить auxk и auxj форсировать его;простой способ - добавить предложение типа auxj _ _ c _ | seq c False = undefined в начало определения.( guard всегда проверяется, заставляя c быть оцененным, но всегда приводит к False, поэтому правая часть никогда не оценивается.)

Лично я быПредложите сохранить шаблон взрыва в окончательной версии, так как он более читабелен, но f c _ | seq c False = undefined будет работать так же хорошо.

1 См. Элегантное запоминание с функциональными попытками запоминания и библиотекой данных-мемокомбинаторов .

2 Действительно, GHC часто может даже исключить промежуточную структуру полностью , используя fusion и вырубку леса , создавая машинный код, подобный тому, как вычисление будет записано в низкоуровневый императивный язык.

3 Хотя, если у вас есть такие циклы, вполне возможно, что этот стиль программирования поможет вам упростить их - лень означает, что вы можете легко разделить независимые части вычисления на отдельные структуры, а затем отфильтровать и объединять их, не беспокоясь о том, что вы будете дублировать работу, делая промежуточные вычисления, которые впоследствии будут отброшены.

2 голосов
/ 06 февраля 2012

ОК, давайте поработаем с нуля.

У вас есть список записей

entries = [(k,j) | j <- [0..jmax], k <- [0..kmax]]

И на основании этих индексов у вас есть тесты и количество

tests m n = map (\(k,j) -> j + k == m + n - 3) entries
counts = map (\(_,j) -> if (rem j 3) == 0 then 2 else 1) entries

Теперь вы хотите построить две вещи: «общее» количество и список записей, которые «проходят» тест. Проблема, конечно, в том, что вы хотите генерировать последние лениво, в то время как первые (чтобы не взорвать стек) должны оцениваться строго.

Если вы оцениваете эти две вещи по отдельности, то вы должны либо 1) запретить совместное использование entries (сгенерировать его дважды, один раз для каждого вычисления), либо 2) сохранить весь список entries в памяти. Если вы оцениваете их вместе, то вы должны либо: 1) строго оценить, либо 2) иметь много стекового пространства для огромного блока, созданного для подсчета. Вариант № 2 в обоих случаях довольно плохой. Ваше императивное решение решает эту проблему просто путем оценки одновременно и строго. Для решения в Haskell вы можете выбрать вариант № 1 для отдельной или одновременной оценки. Или вы могли бы показать нам свой «настоящий» код и, возможно, мы могли бы помочь вам найти способ изменить ваши зависимости данных; может оказаться, что вам не нужно общее количество или что-то в этом роде.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...