Степень оптимизации GHC

Я не очень хорошо знаком с тем, что Haskell/GHC может оптимизировать код. Ниже у меня есть довольно "грубая сила" (в декларативном смысле) реализация проблемы n queens. Я знаю, что его можно написать более эффективно, но это не мой вопрос. Это заставило меня задуматься о возможностях и ограничениях GHC.

Я выразил это в том, что я считаю довольно простым декларативным смыслом. Фильтрующие перестановки [1..n], которые соответствуют предикату For all indices i,j s.t j<i, abs(vi - vj) != j-i Я бы надеялся, что это то, что может быть оптимизировано, но это также похоже на то, чтобы просить много компилятора.

validQueens x = and [abs (x!!i - x!!j) /= j-i | i<-[0..length x - 2], j<-[i+1..length x - 1]] 

queens n = filter validQueens (permutations [1..n])

oneThru x = [1..x]    
pointlessQueens = filter validQueens . permutations . oneThru

main = do
          n <- getLine 
          print $ pointlessQueens $ (read :: String -> Int) n

Это работает довольно медленно и быстро растет. n=10 занимает около секунды, а n=12 - навсегда. Без оптимизации я могу сказать, что рост является факториалом (# перестановок), умноженным на квадратичное (# различий в предикате для проверки). Есть ли способ, которым этот код может работать лучше, чем интеллектуальная компиляция? Я пробовал основные ghc опции, такие как -O2 и не заметил существенной разницы, но я не знаю более тонких точек (просто добавил flagS)

Мое впечатление, что функция я call queens не может быть оптимизирована и должна генерировать все перестановки перед фильтром. У бесплатной версии есть больше шансов? С одной стороны, я чувствую, что умное понимание функций между фильтром и предикатом может сбить некоторые явно нежелательные элементы, прежде чем они будут полностью сгенерированы, но, с другой стороны, это похоже на то, что нужно много спросить.

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

  • Возможно ли более оптимизированная версия вышеописанной функции оптимизировать?
  • Какие шаги я могу предпринять для создания/компиляции/времени ссылки для оптимизации оптимизации?
  • Можете ли вы вкратце описать возможные (и противопоставить невозможным!) средствам оптимизации для вышеуказанного кода? В какой момент процесс происходит?
  • Есть ли какая-то конкретная часть вывода ghc --make queensN -O2 -v, на которую я должен обратить внимание? Мне ничего не стоит. Даже не вижу большой разницы в выходе из-за флагов оптимизации.

Я не слишком озабочен этим примером кода, но я думал, что это заставляет меня думать, и мне кажется, что это достойный автомобиль для обсуждения оптимизации.

PS - permutations находится из Data.List и выглядит следующим образом:

permutations            :: [a] -> [[a]]
permutations xs0        =  xs0 : perms xs0 []
  where
    perms []     _  = []
    perms (t:ts) is = foldr interleave (perms ts (t:is)) (permutations is)
      where interleave    xs     r = let (_,zs) = interleave' id xs r in zs
            interleave' _ []     r = (ts, r)
            interleave' f (y:ys) r = let (us,zs) = interleave' (f . (y:)) ys r
                                     in  (y:us, f (t:y:us) : zs)

Ответ 1

На более общем уровне относительно "каких оптимизаций может работать GHC", это может помочь немного разобраться в идее "оптимизации". Существуют концептуальные различия, которые могут быть сделаны между аспектами программы, которую можно оптимизировать. Например, рассмотрим:

  • Внутренняя логическая структура алгоритма: вы можете смело предположить почти в каждом случае, что это никогда не будет оптимизировано. Вне экспериментальных исследований вы вряд ли найдете компилятор, который заменит сортировку пузыря на сортировку слияния или даже сортировку вставки, и крайне маловероятно найти тот, который заменит bogosort с чем-то разумным.

  • Необязательная логическая структура алгоритма: например, в выражении g (f x) (f x) сколько раз будет вычислено f x? Как насчет выражения типа g (f x 2) (f x 5)? Они не являются неотъемлемой частью алгоритма, и различные варианты могут быть взаимозаменяемы, не влияя ни на что иное, кроме производительности. Трудности при выполнении оптимизации здесь по существу признают, что фактически можно заменить замещение, не изменяя значения, и предсказать, какая версия будет иметь наилучшие результаты. В эту категорию входит множество ручных оптимизаций, а также множество умений GHC.

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

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

  • Детали низкого уровня: такие вещи, как макет памяти и другие аспекты конечного кода. Они имеют тенденцию быть несколько загадочными и сильно зависят от деталей реализации среды выполнения, ОС и процессора. Оптимизации такого рода, по сути, являются причиной того, что у нас есть компиляторы, и обычно это не то, о чем вам нужно беспокоиться, если вы не пишете код, который очень требователен к вычислениям (или сами пишете компилятор).

Что касается вашего конкретного примера: GHC не будет существенно изменять внутреннюю сложность вашего алгоритма. Возможно, он сможет удалить некоторые постоянные факторы. То, что он не может сделать, это применять улучшения с постоянным коэффициентом, которые не могут быть верными, особенно те, которые технически меняют смысл программы так, как вам неинтересно. Примером здесь является ответ @sclv, в котором объясняется, как ваше использование print создает ненужные служебные данные; там ничего не может сделать GHC, и на самом деле текущая форма, возможно, будет препятствовать другим оптимизации.

Ответ 2

Здесь есть концептуальная проблема. Перестановки создают потоковые перестановки, а фильтр также поточно. Что заставляет все преждевременно "показывать", подразумеваемое в "печати". Измените свою последнюю строку на:

mapM print $ pointlessQueens $ (read :: String -> Int) n

и вы увидите, что результаты генерируются потоковым способом намного быстрее. Это фиксирует для больших результирующих наборов потенциальную утечку пространства и, кроме того, позволяет просто печатать, как вычисляемые, а не все сразу в конце.

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

Изменить. Как показано ниже, шоу luqui также показывает потоковое вещание (или, по крайней мере, шоу [Int]), но буферизация строк тем не менее затрудняет просмотр подлинной скорости вычислений...

Ответ 3

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

В случае вашего вопроса, глупо говорить о возможной/невозможной оптимизации, флагов компилятора и о том, как наилучшим образом сформулировать его и т.д., когда улучшение алгоритма смотрит на нас так откровенно в лицо.

Одной из первых вещей, которые будут проверяться, являются перестановки, начинающиеся с первой королевы в позиции 1 и второй королевы в позиции 2 ([1,2...]). Это, конечно, не решение, и нам придется переместить одну из королев. Однако в вашей реализации будут проверены все перестановки с участием этой комбинации двух первых ферзей! Поиск должен прекратиться и мгновенно перейти к перестановкам с участием [1,3,...].

Вот версия, которая выполняет эту сортировку:

import Data.List
import Control.Monad

main = getLine >>= mapM print . queens . read

queens :: Int -> [[Int]]
queens n = queens' [] n

queens' xs n 
 | length xs == n = return xs 
 | otherwise = do 
  x <- [1..n] \\ xs
  guard (validQueens (x:xs))
  queens' (x:xs) n

validQueens x = 
  and [abs (x!!i - x!!j) /= j-i | i<-[0..length x - 2], j<-[i+1..length x - 1]]

Ответ 4

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

Первая статья, которую я знаю о том, как это сделать для проблемы n queens в ленивом функциональном языке, - это статья Тернера "Рекурсионные уравнения как язык программирования". Вы можете прочитать ее в Google Books здесь.

С точки зрения вашего комментария о шаблоне, который стоит запомнить, эта проблема представляет собой очень мощный шаблон. Большой документ по этой идее - документ Филиппа Вадлера "Как заменить неудачу в списке успехов", который можно прочитать в Google Books здесь

Вот чистая, не монадическая реализация, основанная на реализации Тернера Миранды. В случае n = 12 (queens 12 12) он возвращает первое решение за 0,01 секунды и будет вычислять все 14 200 решений менее чем за 6 секунд. Конечно, печать займет гораздо больше времени.

queens :: Int -> Int -> [[Int]]
queens n boardsize = 
    queensi n 
        where
          -- given a safe arrangement  of queens in the first n - 1 rows,
          -- "queensi n" returns a list of all the safe arrangements of queens
          -- in the first n rows
          queensi :: Int -> [[Int]]
          queensi 0  = [[]]
          queensi n  = [ x : y | y <- queensi (n-1) , x <- [1..boardsize], safe x y 1]

-- "safe x y n" tests whether a queen at column x would be safe from previous
-- queens in y where the first element of y is n rows away from x, the second
-- element is (n+1) rows away from x, etc.
safe :: Int -> [Int] -> Int -> Bool
safe _ [] _ = True
safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]
-- we only need to check for queens in the same column, and the same diagonals;
-- queens in the same row are not possible by the fact that we only pick one
-- queen per row