我是函数式编程的新手,现在学习 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
为了比较,我还写了一个纯 C 实现。
现在,如果我尝试为足够大的数组运行 Haskell 实现n
,我有:
$ 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
编辑:这种比较并不公平,因为 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 有没有办法严格地构建列表/做列表推导?在这种情况下,让它保持懒惰没有任何好处。
- 在这种情况下,如何减少总体内存使用量?我想,我必须做一些严格的事情,但看不到是什么。换句话说,如果我必须放一些
seq
s和刘海,在哪里以及为什么? - 最后,最重要的是,识别这种昂贵结构的最佳策略是什么?
我确实在Real World Haskell中阅读了有关分析和优化的一章,但目前尚不清楚我如何准确地决定什么应该严格,什么不应该。
请原谅我这么长的帖子。
EDIT2:正如 A. Rex 在评论中所建议的那样,我尝试在 valgrind 中运行这两个程序。这就是我观察到的。对于 Haskell 程序(
n
=200000),它发现:malloc/free:33 次分配,30 次释放,分配了 84,109 字节。...检查了 55,712,980 个字节。
对于 C 程序(经过小修复):
malloc/free:2 次分配,2 次释放,分配了 3,200,000 字节。
因此,看起来虽然 Haskell 分配了小得多的内存块,但它经常这样做,并且由于垃圾收集的延迟,它们会累积并保留在内存中。所以,我还有一个问题:
- 是否有可能在 Haskell 中避免大量的小分配?基本上,要声明,我需要处理整个数据结构,而不仅仅是按需处理它的片段。