我昨天开始关注Haskell,目的是实际学习它。我在编程语言课程中编写了一些简单的程序,但没有一个真正关心效率。我正在尝试了解如何改善以下程序的运行时间。
我的程序解决了以下玩具问题(我知道如果您知道什么是阶乘,那么手动计算答案很简单,但我正在以强制方式使用后继功能):
http://projecteuler.net/problem=24
给定有限长度列表的词典排序的后继函数的算法如下:
如果列表已按降序排列,那么我们在词典排序中有最大元素,因此没有继承者。
给定一个列表h:t,或者在词典排序中t是最大的,或者不是。在后一种情况下,计算t的后继。在前一种情况下,进行如下。
选择t大于h的最小元素d。
用h代替d,给出一个新列表t'。排序中的下一个元素是d :( sort t')
我实现此目的的程序如下(许多这些函数可能在标准库中):
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,我不得不说的一件很酷的事情就是程序在第一次编译时工作正常,所以这部分语言似乎确实填补了它的承诺。
答案 0 :(得分:12)
许多这些功能可能都在标准库中
事实上。如果您import Data.List
sort
可用,maximum
和minimum
可从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秒内完成任务。
然而,问题中提到的聪明算法在更短的时间内以更少的代码解决了任务。