【问题标题】:Using list generator for memory efficient code in Haskell在 Haskell 中使用列表生成器实现内存高效代码
【发布时间】:2016-05-15 06:32:04
【问题描述】:

我想了解如何编写内存高效的haskell 代码。我遇到的一件事是没有简单的方法来制作 python 样式列表生成器/迭代器(我可以找到)。

小例子:

不使用闭式公式求 1 到 100000000 之间的整数之和。

sum(xrange(100000000) 可以用最少的内存快速完成 Python。在 Haskell 中,类似物是 sum [1..100000000]。但是,这会占用大量内存。我认为使用foldlfoldr 会很好,但即使这样也会占用大量内存并且比python 慢。有什么建议吗?

【问题讨论】:

  • Haskell 列表是 lazy,这使得它们比 Python 列表更接近 Python 生成器/迭代器,除了它们的界面看起来像简单列表(因为它们是;Haskell 只是一个懒惰的语言)。
  • 使用 -O2 编译。 Sum 是在源代码中使用折叠实现的。这个总和分配了大约 3 GB 的内存,在我的机器上需要 1.8 秒,大约是 python 的 print sum(xrange(100000001)) 的 3 倍,这就是我感到惊讶的原因。我认为懒惰会解决内存问题,而且速度应该差不多。感谢您的帮助。
  • @Helix 没有懒惰让我们更难理解空间使用情况。请注意,python 的“懒惰”非常有限。当然生成器一次生成一个元素,但是在执行函数调用时,仍然会在执行调用之前评估参数。在 Haskell 中,参数在函数调用之前评估,但只有在调用期间需要该值时才评估。这意味着函数调用链会创建大型未评估结构。
  • btw:如果使用-O2 编译,而sum [...] 将总共分配大约 3gb 是 - 但不是同时(在我的系统上是 44,312 bytes maximum residency
  • 当然和foldl'一样

标签: performance list haskell


【解决方案1】:

TL;DR - 我认为这种情况下的罪魁祸首是 - 将 GHC 默认为 Integer

诚然,我对 python 的了解不够,但我的第一个猜测是 python 仅在必要时才切换到“bigint”——因此所有计算都是在我的机器上使用 Int 又名 64 位整数完成的。

第一次检查

$> ghci
GHCi, version 7.10.3: http://www.haskell.org/ghc/  :? for help
Prelude> maxBound :: Int
9223372036854775807

显示总和的结果 (5000000050000000) 小于该数字,因此我们不必担心 Int 溢出。

我猜你的示例程序大概是这样的

sum.py

print(sum(xrange(100000000)))

sum.hs

main :: IO ()
main = print $ sum [1..100000000]

为了让事情更明确,我添加了类型注释(100000000 :: Integer),用它编译

$ > stack build --ghc-options="-O2 -with-rtsopts=-sstderr"

并运行您的示例,

$ > stack exec -- time sum
5000000050000000
   3,200,051,872 bytes allocated in the heap
         208,896 bytes copied during GC
          44,312 bytes maximum residency (2 sample(s))
          21,224 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      6102 colls,     0 par    0.013s   0.012s     0.0000s    0.0000s
  Gen  1         2 colls,     0 par    0.000s   0.000s     0.0001s    0.0001s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    1.725s  (  1.724s elapsed)
  GC      time    0.013s  (  0.012s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    1.739s  (  1.736s elapsed)

  %GC     time       0.7%  (0.7% elapsed)

  Alloc rate    1,855,603,449 bytes per MUT second

  Productivity  99.3% of total user, 99.4% of total elapsed

1.72user 0.00system 0:01.73elapsed 99%CPU (0avgtext+0avgdata 4112maxresident)k

确实重现了大约 3GB 的内存消耗。

将注释更改为(100000000 :: Int) - 彻底改变了行为

$ > stack build
$ > stack exec -- time sum
5000000050000000
          51,872 bytes allocated in the heap
           3,408 bytes copied during GC
          44,312 bytes maximum residency (1 sample(s))
          17,128 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         0 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         1 colls,     0 par    0.000s   0.000s     0.0001s    0.0001s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.034s  (  0.034s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    0.036s  (  0.035s elapsed)

  %GC     time       0.2%  (0.2% elapsed)

  Alloc rate    1,514,680 bytes per MUT second

  Productivity  99.4% of total user, 102.3% of total elapsed

0.03user 0.00system 0:00.03elapsed 91%CPU (0avgtext+0avgdata 3496maxresident)k
0inputs+0outputs (0major+176minor)pagefaults 0swaps

感兴趣的朋友

如果您使用 conduitvector 之类的库(装箱和未装箱),haskell 版本的行为不会有太大变化。

示例程序

sumC.hs

import Data.Conduit
import Data.Conduit.List as CL

main :: IO ()
main = do res <- CL.enumFromTo 1 100000000 $$ CL.fold (+) (0 :: Int)
          print res

sumV.hs

import           Data.Vector.Unboxed as V
{-import           Data.Vector as V-}

main :: IO ()
main = print $ V.sum $ V.enumFromTo (1::Int) 100000000

使用的版本足够有趣

main = print $ V.sum $ V.enumFromN (1::Int) 100000000

比上述情况更糟 - 即使documentation 另有说明。

enumFromN :: (Unbox a, Num a) => a -> Int -> Vector a

O(n) 产生一个给定长度的向量,其中包含值 x, x+1 等等。这个操作通常比 enumFromTo 更有效。

更新

@Carsten 的评论让我很好奇 - 所以我查看了整数的来源 - 准确地说是 integer-simple,因为对于 Integer 存在其他版本 integer-gmpinteger-gmp2 使用 libgmp

data Integer = Positive !Positive | Negative !Positive | Naught

-------------------------------------------------------------------
-- The hard work is done on positive numbers

-- Least significant bit is first

-- Positive's have the property that they contain at least one Bit,
-- and their last Bit is One.
type Positive = Digits
type Positives = List Positive

data Digits = Some !Digit !Digits
            | None
type Digit = Word#

data List a = Nil | Cons a (List a)

所以当使用Integer 时,与Int 或者更确切地说是未装箱的Int# 相比,内存开销相当大 - 我想这应该被优化,(尽管我还没有确认)。

所以Integer 是(如果我计算正确的话)

  • 1 x Word 用于 sum-type-tag(此处为 Positive
  • n x (Word + Word) 代表SomeDigit 部分
  • 1 x Word 最后一个None

计算中每个 Integer 的内存开销为 (2 + floor(log_10(n)) + 累加器的内存开销更大。

【讨论】:

  • 哇很有趣——我能理解时间上的差异,但内存消耗的差异让我感到困惑
  • @Carsten 我也试着回答你的问题 - 但我不确定一切是否正确,我绝不是该主题的专家
  • @epsilonhalbe: IIRC, integer-gmp 使用了一些东西 data Integer = Positive !ByteStuff# | Negative !ByteStuff# | SmallInt !Int#。因此,对于小整数,它应该执行与Int 相同的功能,但检查(+) 是否需要比Int# 提供的更多空间可能会带来额外的损失(我不确定从SmallInt 切换到Positive 的条件)。
猜你喜欢
  • 2017-12-13
  • 2011-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-12
  • 2014-08-23
  • 1970-01-01
相关资源
最近更新 更多