优化Haskell程序

时间:2013-01-02 04:38:56

标签: optimization haskell

我昨天开始关注Haskell,目的是实际学习它。我在编程语言课程中编写了一些简单的程序,但没有一个真正关心效率。我正在尝试了解如何改善以下程序的运行时间。

我的程序解决了以下玩具问题(我知道如果您知道什么是阶乘,那么手动计算答案很简单,但我正在以强制方式使用后继功能):

http://projecteuler.net/problem=24

给定有限长度列表的词典排序的后继函数的算法如下:

  1. 如果列表已按降序排列,那么我们在词典排序中有最大元素,因此没有继承者。

  2. 给定一个列表h:t,或者在词典排序中t是最大的,或者不是。在后一种情况下,计算t的后继。在前一种情况下,进行如下。

  3. 选择t大于h的最小元素d。

  4. 用h代替d,给出一个新列表t'。排序中的下一个元素是d :( sort t')

  5. 我实现此目的的程序如下(许多这些函数可能在标准库中):

    max_list :: (Ord a) => [a] -> a
    max_list []     = error "Empty list has no maximum!"
    max_list (h:[]) = h
    max_list (h:t)  = max h (max_list t)
    
    min_list :: (Ord a) => [a] -> a
    min_list []     = error "Empty list has no minimum!"
    min_list (h:[]) = h
    min_list (h:t)  = min h (min_list t)
    
    -- replaces first occurrence of x in list with y
    replace :: (Eq a) => a -> a -> [a] -> [a]
    replace _ _ []  = []
    replace x y (h:t)
        | h == x    = y : t
        | otherwise = h : (replace x y t)
    
    -- sort in increasing order
    sort_list :: (Ord a) => [a] -> [a]
    sort_list []    = []
    sort_list (h:t) = (sort_list (filter (\x -> x <= h) t))
                   ++ [h]
                   ++ (sort_list (filter (\x -> x > h) t))
    
    -- checks if list is in descending order
    descending :: (Ord a) => [a] -> Bool
    descending []     = True
    descending (h:[]) = True
    descending (h:t)
        | h > (max_list t) = descending t
        | otherwise        = False
    
    succ_list :: (Ord a) => [a] -> [a]
    succ_list []      = []
    succ_list (h:[])  = [h]
    succ_list (h:t)
        | descending (h:t)   = (h:t)
        | not (descending t) = h : succ_list t
        | otherwise = next_h : sort_list (replace next_h h t)
        where next_h = min_list (filter (\x -> x > h) t)
    
    -- apply function n times
    apply_times :: (Integral n) => n -> (a -> a) -> a -> a
    apply_times n _ a
        | n <= 0      = a
    apply_times n f a = apply_times (n-1) f (f a)
    
    main = putStrLn (show (apply_times 999999 succ_list [0,1,2,3,4,5,6,7,8,9]))
    

    现在的实际问题。在注意到我的程序运行了一段时间后,我写了一个等效的C程序进行比较。我的猜测是,对Haskell的惰性求值导致apply_times函数在实际开始评估结果之前在内存中构建一个巨大的列表。我不得不增加运行时的堆栈大小。由于高效的Haskell编程似乎与技巧有关,是否有任何可用于最小化内存消耗的好技巧?如何最大限度地减少复制和垃圾收集,因为列表不断被反复创建,而C实现将完成所有工作。

    由于Haskell被认为是高效的,我想必须有办法吗?关于Haskell,我不得不说的一件很酷的事情就是程序在第一次编译时工作正常,所以这部分语言似乎确实填补了它的承诺。

1 个答案:

答案 0 :(得分:12)

  

许多这些功能可能都在标准库中

事实上。如果您import Data.List sort可用,maximumminimum可从Prelude获得。来自sort的{​​{1}}比准快速排序更有效,特别是因为您在列表中有很多排序的块。

Data.List

是低效的 - descending :: (Ord a) => [a] -> Bool descending [] = True descending (h:[]) = True descending (h:t) | h > (max_list t) = descending t | otherwise = False - 因为它在每一步中遍历整个左尾,但如果列表是下降的,则尾部的最大值必须是它的头部。 在这里有一个很好的结果。它可以防止thunk的积累,因为O(n²)的第三个等式的第一个守卫强制完全评估列表。但是,通过明确强制列表一次,可以更有效地完成这项工作。

succ_list

会使它成为线性的。那个

  

在注意到我的程序运行了一段时间后,我写了一个等效的C程序进行比较。

那不寻常。到目前为止,很少有人会在C中使用链接列表,在此基础上实施惰性评估将是一项艰巨的任务。

在C中编写等效的程序非常简单。在C中,实现算法的自然方式是使用数组和就地变异。这在这里会自动提高效率。

  

我的猜测是,对Haskell的惰性求值会导致apply_times函数在实际开始评估结果之前在内存中构建一个巨大的列表。

不完全是,它构建的是一个巨大的东西,

descending (h:t@(ht:_)) = h > ht && descending t

并且,在构建了thunk之后,必须对其进行评估。要评估最外层的调用,必须对下一个调用进行足够的评估,以找出最外层调用中哪个模式匹配。因此,最外层的调用被推入堆栈,并开始评估下一个调用。为此,必须确定哪个模式匹配,因此需要第三个调用的部分结果。因此,第二个呼叫被推到堆栈上......最后,您在堆栈上有999998个调用,并开始评估最里面的调用。然后你在每个调用和下一个外部调用之间播放一些乒乓(至少,依赖关系可能会进一步扩展),同时冒泡并从堆栈中弹出调用。

  

是否有任何可用于最小化内存消耗的好技巧

是的,强制中间列表在成为apply_times 999999 succ_list [0,1,2,3,4,5,6,7,8,9] ~> apply_times 999998 succ_list (succ_list [0 .. 9]) ~> apply_times 999997 succ_list (succ_list (succ_list [0 .. 9])) ~> apply_times 999996 succ_list (succ_list (succ_list (succ_list [0 .. 9]))) ... succ_list (succ_list (succ_list ... (succ_list [0 .. 9])...)) 的参数之前进行评估。你需要在这里完成评估,所以香草apply_times不够好

seq

可以防止thunk的堆积,因此你不需要比import Control.DeepSeq apply_times' :: (NFData a, Integral n) => n -> (a -> a) -> a -> a apply_times' 0 _ x = x apply_times' k f x = apply_times' (k-1) f $!! f x 和计数器中构建的一些短列表更多的内存。

  

如何最大限度地减少复制和垃圾收集,因为在C实现可以执行所有操作时,列表会不断地反复创建。

是的,那仍然会分配(和垃圾收集)很多。现在,GHC 非常善于分配和垃圾收集短期数据(在我的盒子上,它可以轻松地以每MUT秒2GB的速率分配而不会慢),但仍然,不分配所有这些列表会更快。

因此,如果您想推送它,请使用就地变异。

上的工作
succ_list

或未装箱的可变Vector(我更喜欢STUArray s Int Int 包提供的界面,但大多数更喜欢array界面;就性能而言,vector包有很多内置的优化,如果你使用vector包,你必须自己编写快速代码,但编写良好的代码在所有实际用途中都是相同的。)


我现在做了一些测试。我没有测试原始的惰性array,只测试apply_times的每个应用deepseq,并将所有涉及的实体的类型修复为f

通过该设置,将Int替换为sort_list可将运行时间从1.82秒减少到1.65(但增加了分配的字节数)。没有太大的区别,但是这些列表不够长,不足以使准快速入口的坏案真的咬人。

最大的不同来自改变Data:list.sort的建议,将时间降低到0.48秒,Alloc率为每MUT秒2,170,566,037字节,0.01秒GC时间(然后使用descending代替sort_list将时间缩短到0.58秒。)

用更简单的sort替换列表结尾段的排序 - 算法保证在排序时按降序排序 - 将时间缩短到0.43秒。

算法的相当直接的翻译,以使用未装箱的可变数组,

reverse

在0.15秒内完成。通过更简单的零件反转来替换分类,将其降低到0.11。

将算法拆分为小型顶级函数,每个函数执行一项任务,使其更具可读性,但这需要付出代价。需要在函数之间传递更多参数,因此并非所有参数都可以在寄存器中传递,并且一些传递的参数 - 数组边界和元素数 - 根本不被使用,因此传递了自重。在{-# LANGUAGE BangPatterns #-} module Main (main) where import Data.Array.ST import Data.Array.Base import Control.Monad.ST import Control.Monad (when, replicateM_) sortPart :: STUArray s Int Int -> Int -> Int -> ST s () sortPart a lo hi | lo < hi = do let lscan !p h i | i < h = do v <- unsafeRead a i if p < v then return i else lscan p h (i+1) | otherwise = return i rscan !p l i | l < i = do v <- unsafeRead a i if v < p then return i else rscan p l (i-1) | otherwise = return i swap i j = do v <- unsafeRead a i unsafeRead a j >>= unsafeWrite a i unsafeWrite a j v sloop !p l h | l < h = do l1 <- lscan p h l h1 <- rscan p l1 h if (l1 < h1) then (swap l1 h1 >> sloop p l1 h1) else return l1 | otherwise = return l piv <- unsafeRead a hi i <- sloop piv lo hi swap i hi sortPart a lo (i-1) sortPart a (i+1) hi | otherwise = return () descending :: STUArray s Int Int -> Int -> Int -> ST s Bool descending arr lo hi | lo < hi = do let check i !v | hi < i = return True | otherwise = do w <- unsafeRead arr i if w < v then check (i+1) w else return False x <- unsafeRead arr lo check (lo+1) x | otherwise = return True findAndReplace :: STUArray s Int Int -> Int -> Int -> ST s () findAndReplace arr lo hi | lo < hi = do x <- unsafeRead arr lo let go !mi !mv i | hi < i = when (lo < mi) $ unsafeWrite arr mi x >> unsafeWrite arr lo mv | otherwise = do w <- unsafeRead arr i if x < w && w < mv then go i w (i+1) else go mi mv (i+1) look i | hi < i = return () | otherwise = do w <- unsafeRead arr i if x < w then go i w (i+1) else look (i+1) look (lo+1) | otherwise = return () succArr :: STUArray s Int Int -> Int -> Int -> ST s () succArr arr lo hi | lo < hi = do end <- descending arr lo hi if end then return () else do needSwap <- descending arr (lo+1) hi if needSwap then do findAndReplace arr lo hi sortPart arr (lo+1) hi else succArr arr (lo+1) hi | otherwise = return () solution :: [Int] solution = runST $ do arr <- newListArray (0,9) [0 .. 9] replicateM_ 999999 $ succArr arr 0 9 getElems arr main :: IO () main = print solution 中使所有其他函数使用局部函数可以减少总体分配和运行时间(排序时为0.13秒,反向时为0.09),因为现在只需要传递必要的参数。

进一步偏离给定的算法并使其恢复正常,

solution

我们可以在0.02秒内完成任务。

然而,问题中提到的聪明算法在更短的时间内以更少的代码解决了任务。