Отладка бесконечных циклов в программах на Haskell с помощью GHCi
Впервые я столкнулся с бесконечным циклом в программе на Haskell, которую я пишу. Я сузил его до довольно специфического раздела кода, но я не могу точно определить, где именно у меня есть неразрывное рекурсивное определение. Я смутно знаком с:trace и:history в GHCi, но проблема в том, что некоторые ветви моего кода содержат довольно много рекурсивных модификаций Data.Map.Map
в том смысле, что карта x
получается adjust
что-то на карте x'
на основе значений в другой карте в зависимости от x'
, Специфика здесь не имеет значения, но, как вы, вероятно, можете сказать, если это происходит переплетенным рекурсивным способом, моя история вызовов полностью увязает во всех различных сравнениях, связанных с картой. lookup
s, adjust
ments и insert
ионов.
Кто-нибудь может порекомендовать более эффективный способ определения местоположения бесконечных циклов? Это, например, очень помогло бы ограничить историю звонков звонками из одного исходного файла.
6 ответов
Убедитесь, что вы в полной мере использовали отладчик GHCi, включая параметр -fbreak-on-exception (полезно, если вы получаете <<loop>>
, да?) и убедитесь, что вы воспользовались советом Стивена об использовании предупреждений GHC.
Если они терпят неудачу (отладчик GHCi действительно не должен 'терпеть неудачу', это просто вопрос интерпретации данных), тогда попробуйте запустить HPC в цикле, чтобы вы могли визуально видеть ветви и значения, которые не оцениваются, если это цикл тогда что-то, что должно быть сделано, вероятно, даже не оценивается, и это будет отображаться в размеченном HTML.
Я удивлен, что никто не упомянул вездесущий ответ, который получают все проблемы производительности Haskell (бесконечное время выполнения - довольно экстремальный пример "проблемы производительности"): профилирование!
Я просто смог быстро идентифицировать бесконечный цикл, используя профилирование. Для полноты, скомпилируйте с -prof -fprof-auto
, затем запустите программу на достаточное время, чтобы нарушающая функция была видна в статистике профилирования. Например, я ожидал, что моя программа завершится менее чем за 1 секунду, поэтому я позволил профилировщику работать около 30 секунд, а затем убил свою программу с помощью Ctrl+C. (Примечание. Профилирование сохраняет инкрементные результаты, поэтому вы все равно можете получить значимые данные, даже если вы убьете программу до ее запуска. EDIT: за исключением случаев, когда этого не происходит.)
В файле.prof я обнаружил следующий блок:
individual inherited
COST CENTRE MODULE no. entries %time %alloc %time %alloc
...
primroot.\ Zq 764 3 10.3 13.8 99.5 100.0
primroot.isGen Zq 1080 50116042 5.3 6.9 89.2 86.2
primroot.isGen.\ Zq 1087 50116042 43.4 51.7 83.8 79.3
fromInteger ZqBasic 1088 0 40.4 27.6 40.4 27.6
Таким образом, 50 миллионов записей primroot.isGen
и следующая наиболее часто вызываемая функция имела только 1024 вызова. Кроме того, 99,5% времени выполнения было потрачено на вычисления primroot
, что кажется весьма подозрительным. Я проверил эту функцию и быстро нашел ошибку, в моем случае простая опечатка: (`div` foo)
вместо (div foo)
,
Я думаю, что стоит отметить, что предупреждения GHC не поймали бы эту проблему, ни -fbreak-on-exceptions
, База кода огромна; Попытка отследить проблему, вставив отладочные операторы (любого рода), ни к чему не привела. Мне также не удалось использовать отладчик GHCi, потому что история по существу не существовала, и HPC не обнаружил ничего полезного.
Как говорит ShiDoiSi, решение "на глаз" часто является наиболее успешным способом.
Если вы связываете разные переменные с одинаковыми именами x, x'и т. Д. В одних и тех же функциях, вы можете попробовать включить предупреждения в верхней части вашего файла:
{-# OPTIONS -Wall #-}
Если это проблема, когда вы привязываетесь к неправильной вещи и совершаете убегающую рекурсию, это может помочь вам обнаружить ее, например, указав на неожиданное использование теневого копирования.
Я нахожусь в середине долгого сеанса отладки, чтобы найти причину бесконечного цикла. Я очень близко, и это то, что помогло мне больше всего. Предположим, что ваш цикл вызван чем-то вроде этого:
...
x1 = f1 x2 y
x2 = f2 z x3
x3 = f3 y x1
...
Таким образом, x1 зависит от x2, который зависит от x3, который зависит от x1. ПЛОХОЙ!
Посыпать следовые функции в определениях f1, f2, f3. Что-то вроде:
f1 x y | trace ("f1: ") False = undefined
f1 x y = ... -- definition of f1
f2 x y | trace ("f2: ") False = undefined
f2 x y = ... -- definition of f2
-- same for f3
Запустите вашу программу, чтобы увидеть, какая из этих функций вызывается. Выход может быть что-то вроде
f3:
f2:
f1:
<<loop>>
Затем начните показывать некоторые переменные в функциях трассировки. Например, если вы измените след f2 на
f2 x y | trace ("f2: x: " ++ show x) False = undefined
тогда вывод будет выглядеть примерно так:
f3:
f2: x: x_value
f1:
<<loop>>
Но если вы затем измените след f2 на
f2 x y | trace ("f2: x: " show x ++ " y: " ++ show y) False = undefined
Тогда вывод будет
f3:
<<loop>>
потому что второй аргумент f2 не может быть оценен из-за круговой зависимости.
Итак, теперь вы знаете, что одной из функций в бесконечном цикле является f2 и что ее второй аргумент (но не первый) имеет круговую зависимость.
Удачной отладки!
Взломав довольно много строк на Haskell, я должен сказать, что мне никогда не удавалось с отладкой. Всегда была тщательная доработка кода и рефакторинг, который в конечном итоге помог мне отследить проблему. Но, конечно, это просто анекдотично.
Разве вы не можете использовать: назад и: вперед, чтобы посетить свою историю / трассировку и выяснить эволюцию ваших карт между вызовами?
Вы должны быть в состоянии определить шаблон, который приводит к рекурсивному циклу.
- Если это слишком сложно, вы, возможно, достигли точки, когда вы пишете какой-то код, слишком умный для отладки (или, может быть, слишком сложный, и вы должны реорганизовать его ^^)-