繁体   English   中英

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

[英]Partial application memory management in Haskell

我有一个函数ascArr :: String -> BigData用于从字符串中解析一些大的严格数据,而另一个altitude :: BigData -> Pt -> Maybe Doublealtitude :: 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 :内存使用率上升开头(分配的UArray在BigData内存时),那么它是恒定的,而解析,然后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优化。

谢谢。

您可以通过查看GHC 生成的核心来了解发生了什么。 优化核心的评估语义非常可预测(与Haskell本身不同),因此它通常是性能分析的有用工具。

我用GHC 7.10.3的ghc -fforce-recomp -O2 -ddump-simpl file.hs编译了您的代码。 您可以查看完整的输出yoursefl,但是我提取了相关的位:

$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 (忽略定义),字符串才会被触摸。 但是函数中计算“解析”的部分在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 *无关。 这与第二版非常相似-但是现在两个参数都被求值了。 但是,这对性能几乎没有影响,因为存在相同的问题- $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
      }

这是“好”版本。 它需要一个字符串,将$wascArr应用于它,然后不再使用该字符串。 这很关键-如果将此函数部分地应用于字符串,则let w_s = .. in \\w1 -> ...中只剩下let w_s = .. in \\w1 -> ...没有一个提及字符串,因此可以对其进行垃圾回收。 长期存在的参考是w_s ,这是您的“大数据”。 并注意:即使维持这个字符串的引用,它不能被垃圾收集,这个版本仍然是显着更好的-只是因为它不重新计算“分析”在每次调用函数。 这是一个关键缺陷-可以立即垃圾回收字符串的事实是额外的。

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 ...) ... ,则会得到\\w1 -> altitude (ascArr ...) ... ,因此每次调用该函数都会重新计算ascArr 不要紧,你如何使用这一功能-这是行不通的,你想要的方式。

parseAsc5 = parseAsc4

(对我而言)令人惊讶的是,GHC发现parseAsc5parseAsc4 那么这一点应该很明显。


至于为什么 GHC会为此代码生成此特定内核,这真的很难说出来。 在许多情况下,保证共享的唯一方法是在原始代码中进行显式共享。 GHC不执行公共子表达式消除parseAsc3实现手动共享。


*解析器本身也可能存在一些性能问题,但这不是这里的重点。 如果您对Parsec解析器有疑问( Parsec性能考虑),建议您另外提出一个问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM