简体   繁体   English

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

[英]Partial application memory management in Haskell

I have a function ascArr :: String -> BigData for parsing some big strict data from a string and another one, altitude :: BigData -> Pt -> Maybe Double , for getting something useful from the parsed data. 我有一个函数ascArr :: String -> BigData用于从字符串中解析一些大的严格数据,而另一个altitude :: BigData -> Pt -> Maybe Doublealtitude :: BigData -> Pt -> Maybe Double ,用于从解析后的数据中获取有用的东西。 I want to parse the big data once and then use the altitude function with the first argument fixed and the second one varying. 我想解析一次大数据,然后在第一个参数固定且第二个参数变化的情况下使用altitude函数。 Here's the code (TupleSections are enabled): 这是代码(已启用TupleSections):

exampleParseAsc :: IO ()
exampleParseAsc = do
  asc <- readFile "foo.asc"
  let arr = ascArr asc
  print $ map (altitude arr . (, 45)) [15, 15.01 .. 16]

This is all ok. 没关系的 Then I want to connect the two functions together and to use partial application for caching the big data. 然后,我想将两个功能连接在一起,并使用部分应用程序来缓存大数据。 I use three versions of the same function: 我使用相同功能的三个版本:

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)

And I call them like this: 我这样称呼他们:

exampleParseAsc2 :: IO ()
exampleParseAsc2 = do
  asc <- readFile "foo.asc"
  let alt = parseAsc5 asc
  print $ map (alt . (, 45)) [15, 15.01 .. 16]

Only the parseAsc3 works like in the exampleParseAsc : Memory usage rises at the beginning (when allocating memory for the UArray in the BigData), then it is constant while parsing, then altitude quickly evaluates the result and then everything is done and the memory is freed. 只有parseAsc3就像在exampleParseAsc :内存使用率上升开头(分配的UArray在BigData内存时),那么它是恒定的,而解析,然后altitude快速评估结果,然后一切都做,内存被释放。 The other two versions are different: The memory usage rises multiple times until all the memory is consumed, I think that the parsed big data is not cached inside the alt closure. 其他两个版本是不同的:内存使用率会多次上升,直到所有内存都被消耗为止,我认为已解析的大数据未在alt闭包内部进行缓存。 Could someone explain the behaviour? 有人可以解释这种行为吗? Why are the versions 3 and 4 not equivalent? 为什么版本3和版本4不相同? In fact I started with something like parseAsc2 function and just after hours of trial I found out the parseAsc3 solution. 实际上,我是从parseAsc2函数之类的东西开始的,经过数小时的试用,我才找到了parseAsc3解决方案。 And I am not satisfied without knowing the reason... 我不知道原因不满意...

Here you can see all my effort (only the parseAsc3 does not consume whole the memory; parseAsc is a bit different from the others - it uses parsec and it was really greedy for memory, I'd be glad if some one explained me why, but I think that the reason is different than the main point of this question, you may just skip it): 在这里,您可以看到我的所有努力(只有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)

Compiled with GHC 7.10.3, with -O optimization. 与GHC 7.10.3一起编译,并带有-O优化。

Thank you. 谢谢。

You can figure out what's happening by looking at the generated core from GHC. 您可以通过查看GHC 生成的核心来了解发生了什么。 The evaluation semantics of optimized core are very predictable (unlike Haskell itself) so it is often a useful tool for performance analysis. 优化核心的评估语义非常可预测(与Haskell本身不同),因此它通常是性能分析的有用工具。

I compiled your code with ghc -fforce-recomp -O2 -ddump-simpl file.hs with GHC 7.10.3. 我用GHC 7.10.3的ghc -fforce-recomp -O2 -ddump-simpl file.hs编译了您的代码。 You can look at the full output yoursefl but I've extracted the relevant bits: 您可以查看完整的输出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
    }

The code above looks a little funny but is essentially Haskell. 上面的代码看起来有些有趣,但是本质上是Haskell。 Note that the first thing parseAsc2 does is force its second argument to be evaluated (the case statement evaluates the tuple, which corresponds to the pattern match) - but not the string. 请注意, parseAsc2做的第一件事是强制其第二个参数求值(case语句求值的元组,它对应于模式匹配),而不是字符串。 The string won't be touched until deep inside $wParseAsc2 (definition omitted). 直到深入$wParseAsc2 (忽略定义),字符串才会被触摸。 But the part of the function that computes the "parse" is inside the lambda - it will be recomputed for every invocation of the function. 但是函数中计算“解析”的部分在lambda内部-每次调用该函数都会重新计算。 You don't even have to look at what it is - the rules for evaluating core expressions are very prescriptive. 您甚至不必查看它是什么-评估核心表达式的规则非常规范。

$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
    }
    }

The situation with parseAsc has little to do with Parsec*. parseAsc的情况与Parsec *无关。 This is much like version two - now both arguments are evaluated, however. 这与第二版非常相似-但是现在两个参数都被求值了。 This has little effect, however, on the performance, because the same problem is there - $wparseAsc is just a lambda, meaning all the work it does is done at every invocation of the function. 但是,这对性能几乎没有影响,因为存在相同的问题- $wparseAsc只是一个lambda,这意味着它所做的所有工作都是在每次调用函数时完成的。 There can be no sharing. 不能共享。

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
      }

Here is the "good" version. 这是“好”版本。 It takes a string, applies $wascArr to it, and then the string is never used again. 它需要一个字符串,将$wascArr应用于它,然后不再使用该字符串。 This is crucial - if this function is partially applied to a string, you are left with let w_s = .. in \\w1 -> ... - none of this mentions the string, so it can be garbage collected. 这很关键-如果将此函数部分地应用于字符串,则let w_s = .. in \\w1 -> ...中只剩下let w_s = .. in \\w1 -> ...没有一个提及字符串,因此可以对其进行垃圾回收。 The long lived reference is to w_s which is your "big data". 长期存在的参考是w_s ,这是您的“大数据”。 And note: even if a reference to the string was maintained, and it could not be garbage collected, this version would still be substantially better - simply because it does not recompute the "parse" at each invocation of the function. 并注意:即使维持这个字符串的引用,它不能被垃圾收集,这个版本仍然是显着更好的-只是因为它不重新计算“分析”在每次调用函数。 This is the critical flaw - the fact that the string can be garbage collected immediately is extra. 这是一个关键缺陷-可以立即垃圾回收字符串的事实是额外的。

parseAsc4 =
  \ str_a22a pt_a22b ->
    case pt_a22b of _ { (ww1_s8c5, ww2_s8c6) ->
    $waltitude (ascArr str_a22a) ww1_s8c5 ww2_s8c6
    }

Same issue as version two. 与第二版相同。 Unlike version three, if you partially apply this, you get \\w1 -> altitude (ascArr ...) ... , so ascArr is recomputed for every invocation of the function. 与版本3不同,如果部分应用此\\w1 -> altitude (ascArr ...) ... ,则会得到\\w1 -> altitude (ascArr ...) ... ,因此每次调用该函数都会重新计算ascArr It doesn't matter how you use this function - it simply won't work the way you want. 不要紧,你如何使用这一功能-这是行不通的,你想要的方式。

parseAsc5 = parseAsc4

Amazingly (to me), GHC figures out that parseAsc5 is precisely the same as parseAsc4 ! (对我而言)令人惊讶的是,GHC发现parseAsc5parseAsc4 Well this one should be obvious then. 那么这一点应该很明显。


As for why GHC generates this particular core for this code, it really isn't easy to tell. 至于为什么 GHC会为此代码生成此特定内核,这真的很难说出来。 In many cases the only way to guarantee sharing is to have explicit sharing in your original code. 在许多情况下,保证共享的唯一方法是在原始代码中进行显式共享。 GHC does not do common subexpression elimination - parseAsc3 implements manual sharing. GHC不执行公共子表达式消除parseAsc3实现手动共享。


*Maybe the parser itself has some performance issues too, but that isn't the focus here. *解析器本身也可能存在一些性能问题,但这不是这里的重点。 If you have question about your Parsec parser (performance wise, or otherwise) I encourage you to ask a separate question. 如果您对Parsec解析器有疑问( Parsec性能考虑),建议您另外提出一个问题。

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

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