【发布时间】:2010-10-02 08:43:25
【问题描述】:
我是函数式编程的新手,现在学习 Haskell。作为练习,我决定为一维线性扩散方程实施显式欧拉方法。虽然下面的代码可以正常工作,但我对它的性能并不满意。事实上,我关心的是内存消耗。我认为这与惰性评估有关,但无法弄清楚如何减少其内存使用量。
该算法的思想非常简单,用命令式的方式说清楚:它接受一个“数组”,并为每个内部点添加一个值,该值是由该点本身的值组合计算得出的和它的邻居。边界点是特殊情况。
所以,这是我的 Euler1D.hs 模块:
module Euler1D
( stepEuler
, makeu0
) where
-- impose zero flux condition
zeroflux :: (Floating a) => a -> [a] -> [a]
zeroflux mu (boundary:inner:xs) = [boundary+mu*2*(inner-boundary)]
-- one step of integration
stepEuler :: (Floating a) => a -> [a] -> [a]
stepEuler mu u@(x:xs) = (applyBC . (diffused mu)) u
where
diffused mu (left:x:[]) = [] -- ignore outer points
diffused mu (left:x:right:xs) = -- integrate inner points
(x+mu*(left+right-2*x)) : diffused mu (x:right:xs)
applyBC inner = (lbc u') ++ inner ++ (rbc u') -- boundary conditions
where u' = [head u] ++ inner ++ [last u]
lbc = zeroflux mu -- left boundary
rbc = (zeroflux mu) . reverse -- right boundary
-- initial condition
makeu0 :: Int -> [Double]
makeu0 n = [ ((^2) . sin . (pi*) . xi) x | x <- [0..n]]
where xi x = fromIntegral x / fromIntegral n
还有一个简单的 Main.hs:
module Main where
import System ( getArgs )
import Euler1D
main = do
args <- getArgs
let n = read $ head args :: Int
let u0 = makeu0 n
let un = stepEuler 0.5 u0
putStrLn $ show $ sum un
为了比较,我还写了a pure C implementation。
现在,如果我尝试为足够大的数组 n 运行 Haskell 实现,我有:
$ time ./eulerhs 200000
100000.00000000112
real 0m3.552s
user 0m3.304s
sys 0m0.128s
相比之下,C 版本快了近两个数量级:
$ time ./eulerc 200000
100000
real 0m0.088s
user 0m0.048s
sys 0m0.008s
EDIT:这个比较并不公平,因为 Haskell 版本是 使用分析标志和 C 编译 不是。如果我编译这两个程序 使用
-O2并且两者都没有分析 标志,我可以增加n。在这个 案例time ./eulerhs 1000000需要 0m2.236s,而time ./eulerc 1000000只需要 0m0.293s。所以 问题仍然存在 优化且无需分析, 它只是偏移量。我还想指出,那段记忆 Haskell程序的分配 似乎与
n呈线性增长。 这应该没问题。
但最糟糕的是内存需求。我的 Haskell 版本需要 超过 100MB(我估计 C 中的最低限度是 4MB)。我认为这可能是问题的根源。根据分析报告,该程序在 GC 上花费了 85% 的时间,并且
total time = 0.36 secs (18 ticks @ 20 ms)
total alloc = 116,835,180 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
makeu0 Euler1D 61.1 34.9
stepEuler Euler1D 33.3 59.6
CAF:sum Main 5.6 5.5
我惊讶地发现makeu0 是如此昂贵。我认为这是由于它的惰性评估(如果它的 thunk 保留在内存中直到 stepEuler 结束)。
我在Main.hs 中尝试了这种更改:
let un = u0 `seq` stepEuler 0.5 u0
但没有发现任何区别。我不知道如何减少stepEuler 中的内存使用量。所以,我的问题是:
- Haskell 有没有一种方法可以严格地构建列表/执行列表推导?在这种情况下,让它保持懒惰没有任何好处。
- 在这种情况下如何减少总体内存使用量?我想,我必须做一些严格的事情,但看不到是什么。换句话说,如果我必须放一些
seqs 和刘海,在哪里以及为什么? - 最后,最重要的是,识别此类昂贵构造的最佳策略是什么?
我确实在Real World Haskell 中阅读了有关性能分析和优化的一章,但目前尚不清楚我如何准确地决定什么应该严格,什么不应该。
请原谅我这么长的帖子。
EDIT2:正如 A. Rex 在 cmets 中所建议的,我尝试同时运行这两个 valgrind 中的程序。这就是 我观察到。对于 Haskell 程序 (
n=200000) 它发现:malloc/free:33 次分配,30 次释放,84,109 字节分配。 ... 检查了 55,712,980 字节。
对于 C 程序(经过小修复):
malloc/free:2 次分配,2 次释放,分配 3,200,000 字节。
所以,看起来虽然 Haskell 分配更小的内存块, 它经常这样做,并且由于延迟 垃圾收集,它们积累 并留在记忆中。所以我有 另一个问题:
- 是否可以避免很多 Haskell中的小分配? 基本上,要声明,我需要 处理整个数据结构 而不仅仅是它的片段 需求。
【问题讨论】:
-
对于我来说,当 gcc 和 ghc 都被赋予 -O2 开关时,Haskell 版本的运行速度只慢了 4 倍。
-
奇怪的是,valgrind 报告说 Haskell 使用的内存比 C 版本大大少。 =/ 但我仍然认为这是一个很好的问题,因为在 Haskell 中分析内存性能很困难。我希望有人能给出比我稍微不知情的“为我工作”更好的答案。
-
A. Rex,您可以将此对编译器设置的引用放在自己的答案中,我认为这很有用。
-
如果它只慢 4 倍,我会很高兴。我刚刚尝试在不进行分析的情况下重新编译,当然,它变得更好了,所以我可以增加
n。time ./eulerhs 1000000运行时间为 2.236 秒,time ./eulerc 1000000运行时间为 0.293 秒。我将编辑问题。感谢 cmets! -
致 A. Rex:valgrind 报告说 Haskell 使用的内存比 C 版本少得多。我没有在 C 程序退出时释放分配的数组,这就是 valgrind 抱怨的原因。修复后,valgrind 报告说 Haskell 程序做了更多的小分配......但这给出了一个想法!
标签: haskell functional-programming garbage-collection memory-management lazy-evaluation