Haskell中的部分应用程序内存管理

时间:2016-03-14 23:48:33

标签: haskell

我有一个函数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优化。

谢谢。

1 个答案:

答案 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发现parseAsc5parseAsc4完全相同!那么这个应该是显而易见的。

至于为什么 GHC为这段代码生成了这个特定的核心,它真的不容易分辨。在许多情况下,保证共享的唯一方法是在原始代码中明确共享。 GHC不执行常见的子表达式消除 - parseAsc3实现手动共享。

*也许解析器本身也有一些性能问题,但这不是重点。如果您对Parsec解析器有疑问(表现明智或其他方面),我建议您另外提问。