Неисправности скорости подсчета списка Haskell

Я пытаюсь оптимизировать скорость выполнения своей программы, и я натолкнулся на некоторые интересные результаты, и я надеюсь, что кто-то сможет ответить. Кажется, что внесение небольших изменений в одно из моих понятий списка резко меняет скорость выполнения, но я не знаю почему.

Вот моя программа, как сейчас.

import Data.Ord
import Control.Monad
import Data.Array
import Data.Ix
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List (minimumBy, foldl')


arrayMatrix lists = let rlen = length lists
                        clen = length $ head lists
                        r    = ((1,1), (rlen, clen))
                    in array r . zip (range r) $ concat lists

a_star start goal h m = search S.empty (S.singleton start) 
                        (M.singleton start (m ! start)) 
                        $ M.singleton start (m ! start + h ! start)
    where neighbors (r,c) = filter (inRange $ bounds m) [ (r-1,c), (r,c+1), (r+1,c) , (r,c-1)]
          search closed open gs fs
              | S.null open     = 0
              | current == goal = gs M.! goal
              | otherwise       = let open'   = S.delete current open
                                      closed' = S.insert current closed
                                      neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
                                                , let ts = gs M.! current + m ! n ]
                                      actionable = filter (\(n,ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs
                                      (op',gs',fs') = foldl' (\(o,ng,nf) (n,ts) -> (S.insert n o, M.insert n ts ng, M.insert n (ts + h ! n) nf)) (open',gs,fs) actionable
                                  in search closed' op' gs' fs'
              where current = minimumBy (comparing (fs M.!)) $ S.toList open

main = do
  matrix <- liftM (arrayMatrix . map (read . ('[':) . (++"]")) . lines) 
            $ readFile "matrix.txt"
  let bds       = bounds matrix
      ulim      = snd bds
      heuristic = let m   = minimum $ elems matrix
                    in listArray bds . map (\(r,c) -> (uncurry (+) ulim)-r-c) $ range bds
  print $ a_star (1,1) ulim heuristic matrix

Сейчас программа работает на моем компьютере ~ 350 мс (скомпилирована с GHC 7.8.2-O2) с matrix.txt, предоставленным Проект Эйлера.

Если я изменю соседи из

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]

к

neighbs = [(n, gs M.! current + m ! n) | n <- neighbors current, S.notMember n closed]

время выполнения увеличивается до более 1сек.
Другие незначительные изменения, такие как перемещение фильтра на следующей строке в понимание списка, дают тот же результат: ~ 1 с.
Может ли кто-нибудь объяснить, почему это происходит?

EDIT: похоже, этого не происходит в более ранних версиях GHC. Я пробовал GHC 7.6.3, и каждый из них выполнял примерно то же самое.

Я включил дампы из ghc -O2 -ddump-simpl -dsuppress-all, как это было предложено cdk. Я действительно не знаю, на что я смотрю, поэтому, если кто-то сможет интерпретировать, это будет большой помощью, спасибо.

Ссылка на обе дампы

EDIT2 (Ответ на Прийатам): Я не думаю, что дело. Я изменил

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]
actionable = filter ((n,ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs

к

neighbs = [(n, gs M.! current + m ! n) | n <- neighbors current, S.notMember n closed ]
actionable = filter ((n,!ts) -> S.notMember n open' || ts < (gs M.! n)) neighbs

с помощью BangPatterns, и это все еще работает чуть больше секунды. Фактически, изменение neigbs из

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let ts = gs M.! current + m ! n ]

к

neighbs = [(n, ts) | n <- neighbors current, S.notMember n closed
          , let !ts = gs M.! current + m ! n ]  -- Added bang before ts

увеличивает время выполнения до более чем 1сек.

Ответы

Ответ 1

Здесь можно догадаться, что произошло с let ts = vs. let !ts =. я получил его от просмотра вывода -ddump-stranal (который выгружает аннотации анализа строгости) и чтение Анализатор спроса в GHC.

Разница между let !ts = и let ts = заключается в том, что если ts равно внизу (т.е. undefined), то n не будет оцениваться вообще потому что ts будет оценен первым, и оценка остановится. Это что разница между двумя программами заключается в том, что пара целые числа n строгие и распакованные в одной версии, но не в другой (см. вывод -ddump-stranal и -ddump-simpl; ссылка выше описывает выход).

Как !ts или не !ts влияет на строгость n? Я думаю, что если ts является нижней, тогда программа должна завершиться с ошибкой перед оценкой n или любой из его элементов (я не уверен, что он сам n :: (Int, Int) или его элементы). Поэтому ghc, похоже, делает правильные вещи, чтобы сохранить n нестрогий, когда ts требуется быть строгим, поскольку оценка n первая и, возможно, неудача в другом месте может быть ошибкой.

Далее, как вы вынуждаете !ts не влиять на n? Обратите внимание, что ts не может быть дном без n, являющимся нижним, если либо gs, current, или m, как известно, не являются дном (это все элементы выражения, кроме n) и уже были оценены (я думаю, что M.! и ! могут никогда не дно, не оценивая их аргументы в первую очередь). Поэтому нам нужно наложить условие "ts - это дно n является нижней и уже оценен ", так что ghc знает, что сначала можно оценить n.

Мое решение: добавьте шаблоны ударов в current, gs и m. С моим ghc 7.8.2, это, похоже, решает проблему. Также кажется, что нужно принудительно выполнить только current.

Я не слишком уверен в первоначальном вопросе о перемещении выражения ts в кортеж, но похожее решение работает.

P.S. Обратите внимание, что

filter (\x -> x > 5) [x | x <- [1..10]] == [x | x <- [1..10], x > 5]

поэтому в ваших списках neighbs и actionable было бы проще привнести предикат фильтра в представление самого списка так:

[(n, ts)
| n <- neighbors current
, S.notMember n closed
, let ts = gs M.! current + m ! n
, S.notMember n open' || ts < (gs M.! n)
]

Ответ 2

Это не полный ответ, так как мне не хватает информации о том, как let и контексты списка реализуются внутри.

Каждый элемент в neighbs является кортежем, а в WHNF сумма не оценивается строго. Это оставляет необоснованные громы, которые могут увеличить время выполнения.

Я предлагаю переписать второе определение с помощью seq без использования let, если возможно, посмотреть, падает ли время выполнения (в этом случае этот ответ, вероятно, будет правильным).

Прочитайте этот, чтобы понять, что такое WHNF.