【问题标题】:Extent of GHC's optimizationGHC的优化程度
【发布时间】:2011-09-22 03:51:49
【问题描述】:

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

我的印象是我调用的函数queens 无法优化,必须在过滤之前生成所有排列。免积分版有更好的机会吗?一方面,我觉得过滤器和谓词之间的智能函数理解可能能够在一些明显不受欢迎的元素完全生成之前将其剔除,但另一方面,这感觉有点儿问题。

对不起,如果这看起来很漫无边际,我想我的问题是

  1. 以上功能的pointfree版本是否更容易优化?
  2. 我可以在制作/编译/链接时采取哪些步骤来鼓励优化?
  3. 您能否简要描述一些可能的(并与不可能的对比!)优化上述代码的方法?这些发生在流程的哪个阶段?
  4. 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)

【问题讨论】:

  • 无意义与否应该没有任何区别。一般来说,错误的算法是编译器无法修复的少数问题之一(相对简单的事情除外,例如将递归阶乘转换为特别聪明的编译器的循环)。
  • 你选择了一个错误的算法,ghc 不会为你解决这个问题。
  • 可能只是我,但我觉得这个问题的前提可能有点过于宽泛,无法以任何直接的方式回答:我目前将问题背后的驱动力解释为:什么可以在纯声明式函数代码上进行各种通用的、节省成本的全程序转换,这似乎是一个独立的研究领域,而且有点过于依赖给定的问题域。 @delnan 的观点非常尖锐;即使使用现代 智能 编译器,计算复杂性仍占主导地位。
  • 我认为问题的前提是好的,但在我看来,最好将问题限制在编译器实际做了哪些优化,并提供一个可以接受的算法的代码示例高效的。再说一次,只是我的看法。
  • 顺便说一句,oneThru = enumFromTo 1.

标签: haskell ghc compiler-optimization


【解决方案1】:

这里有一个概念问题。 Permutations 正在生成流式排列,而 filter 也在流式传输。过早强迫一切的是“打印”中隐含的“显示”。将最后一行更改为:

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

您会发现结果以流媒体方式生成的速度要快得多。对于大型结果集,这解决了潜在的空间泄漏问题,除此之外,它只是让事情被打印为计算结果,而不是在最后一次全部打印。

但是,您不应该期望 ghc 优化有任何数量级的改进(您确实得到了一些明显的改进,主要与严格性和折叠有关,但依赖它们会令人恼火)。通常,您会得到恒定的因素。

编辑:正如 luqui 在下面指出的那样,show 也是流式传输的(或者至少 [Int] 的 show 是流式传输的),但是行缓冲仍然使我们更难看到真正的计算速度。 ..

【讨论】:

  • 你是说这只是线路缓冲强迫太多?! show 也在直播。
  • 好点。这根本不是我所期待的,我很高兴你提出来
【解决方案2】:

应该注意,尽管您确实表示这不是您的问题的一部分,但您的代码的最大问题是您没有进行任何修剪。

就您的问题而言,当算法的改进如此明目张胆地摆在我们面前时,谈论可能/不可能的优化、编译器标志以及如何最好地制定它等等感觉很愚蠢。

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

【讨论】:

  • 我主要想知道使用filter 对函数组合的编译器优化是否可以自动实现这种修剪。我当然避免任何命令式逻辑,认为这会危及任何这样的机会。我的想法是短路逻辑与部分构造的排列的某种组合可能会在构造完成之前拒绝那些明显不好的情况(尽管可能无法学会避免所有此类情况)。但是,看起来很明显我有点太有希望了
  • 我真的很喜欢您实现修剪的方式,并且完全不再需要permutations。我可能会接受另一个 bc 你没有告诉我很多关于编译器优化的事情,但如果问题是“更好地重写这段代码”你就搞定了
  • 有趣的是,您的queens' 如何生成所有这些整数排列,并在每一步都将保护条件用作过滤器。看起来像一个值得记住的模式
  • @jon_darkstar,我知道我没有说太多关于编译器优化的内容,这是一个普遍的主题,一个缺点,但我仍然觉得答案有所贡献。至于为什么过滤器不会为你做这个修剪是因为,我相信你已经意识到,虽然惰性评估避免了很多检查,但它仍然必须完成 n!次。编译器无法缩短您的排列列表,因为无法保证在每个 lambda 表达式内只对函数调用进行一次评估。
【解决方案3】:

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

  • 算法的内在逻辑结构:在几乎所有情况下,您都可以放心地假设它永远不会被优化。在实验研究之外,您不太可能找到一个编译器将冒泡排序替换为合并排序,甚至插入排序,也极不可能找到一个编译器将 bogosort 替换为合理的东西。

  • 算法的非本质逻辑结构:例如,在表达式g (f x) (f x)中,f x将被计算多少次?像g (f x 2) (f x 5) 这样的表达式呢?这些不是算法固有的,不同的变体可以互换,而不会影响除了性能之外的任何东西。此处执行优化的困难本质上是识别何时实际上可以在不改变含义的情况下进行替换,以及预测哪个版本将具有最佳结果。很多手动优化都属于这一类,还有很多 GHC 的聪明之处。

    这也是让很多人失望的部分,因为他们看到 GHC 有多聪明,并期望它做得更多。并且由于 GHC 永远不会使事情变得更糟的合理预期,因此具有潜在优化的情况并不少见,这些优化似乎很明显(并且对程序员来说是)GHC 无法应用,因为区分起来很重要相同的转换会显着降低性能的情况。例如,这就是为什么记忆和公共子表达式消除并不总是自动的原因。

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

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

就您的具体示例而言:GHC 不会显着改变算法的内在时间复杂度。它可能能够去除一些常数因素。它不能做的是应用无法确定是否正确的常数因子改进,尤其是那些技术上以您不关心的方式改变程序含义的改进。这里的例子是@sclv's answer,它解释了您对print 的使用如何产生不必要的开销; GHC 对此无能为力,实际上当前的形式可能会抑制其他优化。

【讨论】:

  • 只是一个想法:据我所知,可以从 Haskell 生成 C 源代码,我想知道——也许这样做是有意义的,并且下一次用 GCC 编译? GCC 一直在改进,它最近也得到了所谓的链接时间优化
【解决方案4】:

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

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

就您对值得记住的模式的评论而言,这个问题引入了一个非常强大的模式。关于这个想法的一篇很棒的论文是 Philip Wadler 的论文“How to Replace Failure by a List of Successes”,可以在 Google Books here

中阅读

这是一个基于 Turner 的 Miranda 实现的纯非单子实现。在 n = 12 (queens 12 12) 的情况下,它会在 0.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

【讨论】:

    猜你喜欢
    • 2017-05-23
    • 2018-11-30
    • 2012-06-08
    • 2016-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-12
    相关资源
    最近更新 更多