我有一个函数ascArr :: String -> BigData
用于解析字符串中的一些大的严格数据,另一个函数altitude :: BigData -> Pt -> Maybe Double
用于从解析的数据中获取有用的东西。我想解析大数据一次,然后使用altitude
函数,第一个参数固定,第二个参数变化。这是代码(启用了TupleSections):
exampleParseAsc :: IO ()
exampleParseAsc = do
asc <- readFile "foo.asc"
let arr = ascArr asc
print $ map (altitude arr . (, 45)) [15, 15.01 .. 16]
一切都好。然后我想将两个函数连接在一起并使用部分应用程序来缓存大数据。我使用相同功能的三个版本:
parseAsc3 :: String -> Pt -> Maybe Double
parseAsc3 str = altitude d
where d = ascArr str
parseAsc4 :: String -> Pt -> Maybe Double
parseAsc4 str pt = altitude d pt
where d = ascArr str
parseAsc5 :: String -> Pt -> Maybe Double
parseAsc5 = curry (uncurry altitude . first ascArr)
我称之为:
exampleParseAsc2 :: IO ()
exampleParseAsc2 = do
asc <- readFile "foo.asc"
let alt = parseAsc5 asc
print $ map (alt . (, 45)) [15, 15.01 .. 16]
只有parseAsc3在exampleParseAsc
中工作:内存使用率在开始时上升(在BigData中为UArray分配内存时),然后在解析时保持不变,然后altitude
快速评估结果,然后一切都完成,内存被释放。其他两个版本是不同的:内存使用率上升多次,直到消耗掉所有内存,我认为解析后的大数据不会缓存在alt
闭包内。有人可以解释这种行为吗?为什么版本3和版本4不相同?事实上,我从parseAsc2
函数开始,经过几个小时的试验,我发现了parseAsc3解决方案。如果不知道原因我就不满意了。
在这里你可以看到我所有的努力(只有parseAsc3
没有消耗整个内存; parseAsc
与其他内容有点不同 - 它使用parsec而且它对内存非常贪心,我和如果有人解释了我的原因,我会很高兴,但我认为原因不同于这个问题的主要内容,你可能会跳过它):
type Pt = (Double, Double)
type BigData = (UArray (Int, Int) Double, Double, Double, Double)
parseAsc :: String -> Pt -> Maybe Double
parseAsc str (x, y) =
case parse ascParse "" str of
Left err -> error "no parse"
Right (x1, y1, coef, m) ->
let bnds = bounds m
i = (round $ (x - x1) / coef, round $ (y - y1) / coef)
in if inRange bnds i then Just $ m ! i else Nothing
where
ascParse :: Parsec String () (Double, Double, Double, UArray (Int, Int) Double)
ascParse = do
[w, h] <- mapM ((read <$>) . keyValParse digit) ["ncols", "nrows"]
[x1, y1, coef] <- mapM ((read <$>) . keyValParse (digit <|> char '.'))
["xllcorner", "yllcorner", "cellsize"]
keyValParse anyChar "NODATA_value"
replicateM 6 $ manyTill anyChar newline
rows <- replicateM h . replicateM w
$ read <$> (spaces *> many1 digit)
return (x1, y1, coef, listArray ((0, 0), (w - 1, h - 1)) (concat rows))
keyValParse :: Parsec String () Char -> String -> Parsec String () String
keyValParse format key = string key *> spaces *> manyTill format newline
parseAsc2 :: String -> Pt -> Maybe Double
parseAsc2 str (x, y) = if all (inRange bnds) (is :: [(Int, Int)])
then Just $ (ff * (1 - px) + cf * px) * (1 - py)
+ (fc * (1 - px) + cc * px) * py
else Nothing
where (header, elevs) = splitAt 6 $ lines str
header' = map ((!! 1) . words) header
[w, h] = map read $ take 2 header'
[x1, y1, coef, _] = map read $ drop 2 header'
bnds = ((0, 0), (w - 1, h - 1))
arr :: UArray (Int, Int) Double
arr = listArray bnds (concatMap (map read . words) elevs)
i = [(x - x1) / coef, (y - y1) / coef]
[ixf, iyf, ixc, iyc] = [floor, ceiling] >>= (<$> i)
is = [(ix, iy) | ix <- [ixf, ixc], iy <- [iyf, iyc]]
[px, py] = map (snd . properFraction) i
[ff, cf, fc, cc] = map (arr !) is
ascArr :: String -> BigData
ascArr str = (listArray bnds (concatMap (map read . words) elevs), x1, y1, coef)
where (header, elevs) = splitAt 6 $ lines str
header' = map ((!! 1) . words) header
[w, h] = map read $ take 2 header'
[x1, y1, coef, _] = map read $ drop 2 header'
bnds = ((0, 0), (w - 1, h - 1))
altitude :: BigData -> Pt -> Maybe Double
altitude d (x, y) = if all (inRange bnds) (is :: [(Int, Int)])
then Just $ (ff * (1 - px) + cf * px) * (1 - py)
+ (fc * (1 - px) + cc * px) * py
else Nothing
where (arr, x1, y1, coef) = d
bnds = bounds arr
i = [(x - x1) / coef, (y - y1) / coef]
[ixf, iyf, ixc, iyc] = [floor, ceiling] >>= (<$> i)
is = [(ix, iy) | ix <- [ixf, ixc], iy <- [iyf, iyc]]
[px, py] = map (snd . properFraction) i
[ff, cf, fc, cc] = map (arr !) is
parseAsc3 :: String -> Pt -> Maybe Double
parseAsc3 str = altitude d
where d = ascArr str
parseAsc4 :: String -> Pt -> Maybe Double
parseAsc4 str pt = altitude d pt
where d = ascArr str
parseAsc5 :: String -> Pt -> Maybe Double
parseAsc5 = curry (uncurry altitude . first ascArr)
使用GHC 7.10.3编译,带-O优化。
谢谢。
答案 0 :(得分:4)
您可以通过查看GHC中的generated core来了解发生了什么。优化核心的评估语义是非常可预测的(与Haskell本身不同),因此它通常是性能分析的有用工具。
我使用GHC 7.10.3编译了ghc -fforce-recomp -O2 -ddump-simpl file.hs
代码。您可以查看完整输出,但我已经提取了相关位:
$wparseAsc2
$wparseAsc2 =
\ w_s8e1 ww_s8e5 ww1_s8e6 ->
let { ...
parseAsc2 =
\ w_s8e1 w1_s8e2 ->
case w1_s8e2 of _ { (ww1_s8e5, ww2_s8e6) ->
$wparseAsc2 w_s8e1 ww1_s8e5 ww2_s8e6
}
上面的代码看起来有点滑稽但基本上是Haskell。请注意,parseAsc2
做的第一件事是强制评估它的第二个参数(case语句评估元组,它对应于模式匹配) - 但不是字符串。直到$wParseAsc2
深处(定义省略)才触及字符串。但是计算&#34;解析&#34;的函数的一部分。在lambda中 - 它将在每次调用函数时重新计算。你甚至不必看它是什么 - 评估核心表达的规则是非常规范的。
$wparseAsc
$wparseAsc =
\ w_s8g9 ww_s8gg ww1_s8gi -> ...
parseAsc
parseAsc =
\ w_s8g9 w1_s8ga ->
case w1_s8ga of _ { (ww1_s8gd, ww2_s8gi) ->
case ww1_s8gd of _ { D# ww4_s8gg ->
$wparseAsc w_s8g9 ww4_s8gg ww2_s8gi
}
}
parseAsc
的情况与Parsec *没什么关系。这很像版本2 - 但是现在两个参数都被评估了。然而,这对性能影响不大,因为存在同样的问题 - $wparseAsc
只是一个lambda,这意味着它所做的所有工作都是在每次调用函数时完成的。没有共享。
parseAsc3 =
\ str_a228 ->
let {
w_s8c1
w_s8c1 =
case $wascArr str_a228
of _ { (# ww1_s8gm, ww2_s8gn, ww3_s8go, ww4_s8gp #) ->
(ww1_s8gm, ww2_s8gn, ww3_s8go, ww4_s8gp)
} } in
\ w1_s8c2 ->
case w1_s8c2 of _ { (ww1_s8c5, ww2_s8c6) ->
$waltitude w_s8c1 ww1_s8c5 ww2_s8c6
}
这是&#34;好&#34;版。它需要一个字符串,对它应用$wascArr
,然后再也不再使用该字符串。这是至关重要的 - 如果此函数部分应用于字符串,则会留下let w_s = .. in \w1 -> ...
- 这些都没有提到字符串,因此可以进行垃圾回收。长期参考是w_s
,这是您的&#34;大数据&#34;。请注意:即使对字符串的引用是进行维护,并且无法进行垃圾回收,这个版本仍然会更好 - 只是因为它不会重新计算&#34;解析&#34 ;在每次调用函数时。这是一个关键的缺陷 - 字符串可以立即被垃圾收集这一事实是额外的。
parseAsc4 =
\ str_a22a pt_a22b ->
case pt_a22b of _ { (ww1_s8c5, ww2_s8c6) ->
$waltitude (ascArr str_a22a) ww1_s8c5 ww2_s8c6
}
与第二版相同的问题。与版本3不同,如果您部分应用此项,则会得到\w1 -> altitude (ascArr ...) ...
,因此每次调用函数时都会重新计算ascArr
。 使用此功能并不重要 - 它只是按照您想要的方式工作。
parseAsc5 = parseAsc4
令人惊讶(对我而言),GHC发现parseAsc5
与parseAsc4
完全相同!那么这个应该是显而易见的。
至于为什么 GHC为这段代码生成了这个特定的核心,它真的不容易分辨。在许多情况下,保证共享的唯一方法是在原始代码中明确共享。 GHC不执行常见的子表达式消除 - parseAsc3
实现手动共享。
*也许解析器本身也有一些性能问题,但这不是重点。如果您对Parsec
解析器有疑问(表现明智或其他方面),我建议您另外提问。 子>