GHC优化的范围

时间:2011-06-24 15:53:44

标签: haskell ghc compiler-optimization

我不太熟悉Haskell / GHC可以优化代码的程度。下面我有一个非常“暴力”(在声明意义上)实施n皇后问题。我知道它可以更有效地编写,但那不是我的问题。这让我想到了GHC优化功能和限制。

我已经在我认为非常简单的陈述性意义中表达了这一点。过滤满足谓词For all indices i,j s.t j<i, abs(vi - vj) != j-i的[1..n]的排列我希望这是可以优化的东西,但它也有点像要求很多编译器。

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,并没有注意到显着差异,但我不知道更精细的点(只是添加了标志)

我的印象是我调用queens的函数无法优化,必须在过滤前生成所有排列。无点版本有更好的机会吗?一方面,我觉得过滤器和谓词之间的智能功能理解可能能够在它们甚至完全生成之前敲掉一些明显不受欢迎的元素,但另一方面,这种感觉有点像问题。 / p>

对不起,如果这看起来很乱,我想我的问题是

  1. 上述功能的无点版本是否更能够优化?
  2. 我可以在make / compile / link时采取哪些步骤来鼓励优化?
  3. 您能简单介绍一下可能的(并与不可能的对比!)上述代码的优化方法吗?在这个过程的哪个阶段会发生这些?
  4. 我应该注意ghc --make queensN -O2 -v输出中是否有任何特定部分?没有什么比我更突出的了。由于优化标志,甚至看不出输出的差异
  5. 我并不过分担心这个代码示例,但我认为编写它让我思考,在我看来,它似乎是讨论优化的一个不错的工具。

    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)
    

4 个答案:

答案 0 :(得分:16)

关于“GHC可以做什么样的优化”的更一般的层面,它可能有助于打破“优化”的想法。可以在可以优化的程序的各个方面之间进行概念上的区分。例如,考虑:

  • 算法的内在逻辑结构:几乎在每种情况下都可以安全地假设永远不会进行优化。在实验研究之外,你不太可能找到一个编译器来替换带有合并排序的冒泡排序,甚至是插入排序,并且极不可能找到一个用合理的东西替换bogosort的编译器。

  • 算法的非必要逻辑结构:例如,在表达式g (f x) (f x)中,计算f x的次数是多少?像g (f x 2) (f x 5)这样的表达怎么样?这些并非算法固有的,并且可以互换不同的变体而不会影响除性能之外的任何。这里执行优化的困难基本上是识别何时可以在不改变含义的情况下完成替换,并且预测哪个版本将具有最佳结果。很多手动优化都属于这一类,同时还有很多GHC的聪明才智。

    这也是让很多人参与其中的一部分,因为他们看到GHC是多么聪明,并期望它能做得更多。而且由于合理的期望GHC永远不会让事情变得更糟,所以对于GHC无法应用的潜在优化(并且对于程序员而言)并不常见,因为区分它是不容易的。相同转换会严重降低性能的情况。例如,这就是为什么memoization和公共子表达式消除并不总是自动的。

    这也是GHC具有巨大优势的部分,因为懒惰和纯洁使很多事情变得更容易,我怀疑是什么导致人们像"Optimizing compilers are a myth (except perhaps in Haskell)."那样发表言论,但是甚至对GHC可以做什么也不切实际的乐观。

  • 低级细节:内存布局和最终代码的其他方面。这些往往有些神秘,并且高度依赖于运行时,操作系统和处理器的实现细节。这种优化基本上是为什么我们有编译器,并且通常不需要担心,除非你编写的代码非常计算要求苛刻(或正在编写自己编译)。

就您的具体示例而言:GHC不会显着改变算法的固有时间复杂度。 可能能够删除一些常数因素。它不能做的是应用不能确定正确的常数因素改进,尤其是技术上以您不关心的方式改变程序含义的那些改进。这里的一个例子是@sclv的答案,它解释了你如何使用print创造了不必要的开销; GHC无法做到这一点,事实上目前的形式可能会抑制其他优化。

答案 1 :(得分:8)

这里有一个概念问题。排列正在生成流式排列,过滤器也是流式排列。过早强迫一切的是“印刷”中隐含的“表演”。将您的最后一行更改为:

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

您将看到结果以流式方式生成得更快。对于大型结果集,这可以解决潜在的空间泄漏问题,除此之外,只是让事情以计算方式打印而不是一次性打印出来。

但是,您不应期望ghc优化会有任何数量级的改进(有一些明显的改进,主要是严格和折叠,但它依赖于它们的刺激性)。你通常会得到不变因素。

编辑:正如luqui在下面指出的那样,show也是流式传输(或者至少是[Int]的显示),但是线路缓冲使得更难以看到真正的速度计算...

答案 2 :(得分:6)

应该注意的是,虽然您确实表示它不是您问题的一部分,但您的代码的一个大问题是您不进行任何修剪。

在你的问题的情况下,谈论可能/不可能的优化,编译器标志,以及如何最好地制定它等等,当算法的改进盯着我们如此公然地面对时,感觉很愚蠢。

首先要尝试的是从位置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]]

答案 3 :(得分:2)

我理解您的问题是关于编译器优化,但正如讨论所示,修剪是必要的。

我所知道的关于如何在惰性函数语言中解决n皇后问题的第一篇论文是特纳的论文“递归方程作为编程语言”你可以在Google Books here中阅读它。 / p>

就你对值得记住的模式的评论而言,这个问题引入了一个非常强大的模式。关于这个想法的一篇很好的论文是Philip Wadler的论文“如何用成功列表取代失败”,可以在Google图书中阅读here

这是一个纯粹的非monadic实现,基于Turner的Miranda实现。在n = 12(皇后12 12)的情况下,它返回.01秒内的第一个解决方案,并将在6秒内计算所有14,200个解决方案。当然打印需要更长的时间。

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