【问题标题】:Procedurally generating large list of values in Haskell -- most idiomatic approach? memory management?在 Haskell 中以程序方式生成大量值 - 最惯用的方法?内存管理?
【发布时间】:2013-02-22 09:35:00
【问题描述】:

我有一个函数,它采用一系列随机数/浮点数,并使用它们来生成一个值/结构(即,获取一个随机速度和一个球被抛出的点的位置,并输出它的坐标会降落)。而且我需要连续生成几千个。

我实现所有东西的方式是每次计算都接受一个 stdGen,用它来生成几个数字,然后传递一个新的 stdGen 以允许它链接到另一个。

为了对 10000 个项目执行此操作,我从 generate_item n 创建了一个列表,它基本上输出一个 (value,gen) 元组(该值是我要计算的值),其中 gen 的值是从generate_item n-1获取值所涉及的计算中递归输出的stdGen

但是,在大约一千个结果左右时,该程序的爬行速度似乎非常慢。而且似乎绝对不可扩展。是否与我将所有 generate_item 结果存储在内存中这一事实有关?

或者有没有比我上面描述的在 Haskell 中使用 Monads 或其他方法来解决这个问题的更惯用的方法?

请注意,即使在 ruby​​ 和 python 等高级脚本语言中,从随机值生成算法的代码也会在几秒钟内生成 10k;这些计算并不密集。

代码

-- helper functions that take in StdGen and return (Result,new StdGen)
plum_radius :: StdGen -> (Float,StdGen)
unitpoint   :: Float -> StdGen -> ((Float,Float,Float),StdGen)
plum_speed  :: Float -> StdGen -> (Float,StdGen)

-- The overall calculation of the value
plum_point  :: StdGen -> (((Float,Float,Float),(Float,Float,Float)),StdGen)
plum_point gen  = (((px,py,pz),(vx,vy,vz)),gen_out)
  where
    (r, gen2)         = plum_radius gen
    ((px,py,pz),gen3) = unitpoint r gen2
    (s, gen4)         = plum_speed r gen3
    ((vx,vy,vz),gen5) = unitpoint s gen4
    gen_out           = gen5

-- Turning it into some kind of list
plum_data_list  :: StdGen -> Int -> (((Float,Float,Float),(Float,Float,Float)),StdGen)
plum_data_list seed_gen 0  = plum_point seed_gen
plum_data_list seed_gen i  = plum_point gen2
  where
    (_,gen2)  = plum_data_list seed_gen (i-1)

-- Getting 100 results
main = do
  gen <- getStdGen
  let data_list = map (plum_data_list gen) [1..100]
  putStrLn List.intercalate " " (map show data_list)

【问题讨论】:

  • 您介意发布您正在使用的代码吗,通常更容易理解并帮助创建一个可以使用的示例。
  • 在代码中添加;希望有帮助!
  • plum_data_list 到底是怎么回事,它生成了一堆点并将它们全部扔掉,但第一个?这可能不是您想要的——请参阅我的答案,了解如何编写一个生成随机事物列表的函数。
  • @luqui 好吧,plum_data_list 中的项目是按顺序访问的,所以我想我有点天真地假设它们被缓存了。

标签: haskell idioms


【解决方案1】:

其他发帖者有好点,StdGen 表现不佳,您可能应该尝试使用 State 而不是手动传递生成器。但我认为最大的问题是你的plum_data_list 函数。

它似乎是为了某种查找,但由于它是递归实现的,没有任何记忆,你所做的调用必须递归到基本情况。也就是说,plum_data_list seed_gen 100 需要来自plum_data_list seed_gen 99 的随机生成器,以此类推,直到plum_data_list seed_gen 0。当您尝试生成这些值的列表时,这将为您提供二次性能。

可能更惯用的方法是让plum_data_list seed_gen 生成一个无限的点列表,如下所示:

plum_data_list :: StdGen -> [((Float,Float,Float),(Float,Float,Float))]
plum_data_list seed_gen = first_point : plum_data_list seed_gen'
  where
    (first_point, seed_gen') = plum_point seed_gen

然后你只需要将main 中的代码修改为take 100 $ plum_data_list gen 之类的代码,就可以恢复线性性能了。

【讨论】:

  • 我想你找到了主内存消耗源:)
【解决方案2】:

考虑只使用 mersenne-twister 和 vector-random 包,该包经过专门优化以生成大量高质量随机数据。

列表不适合分配大量数据 - 最好使用打包表示 - 除非您正在流式传输。

【讨论】:

    【解决方案3】:

    首先,您所描述的模式 - 获取一个 StdGen,然后返回一个带有值的元组和另一个 StdGen 以链接到下一个计算 - 完全正确 State monad 编码的模式。重构代码以使用它可能是开始熟悉一元模式的好方法。

    至于你的性能问题,StdGen 是出了名的慢。我在这方面做得不多,但我听说mersenne twister 更快。

    但是,您可能还想发布您的代码,因为在您生成大型列表的情况下,根据您的操作方式,懒惰确实对您有利或不利。但是如果没有看到你在做什么,很难给出具体的建议。一个经验法则,以防万一您来自另一种函数式语言,例如 Lisp - 在生成列表(或其他惰性数据结构 - 例如树,但不是 Int)时,避免 尾递归。它更快的直觉不会转移到惰性语言。例如。使用(没有我在实践中实际使用的单子风格)

    randoms :: Int -> StdGen -> (StdGen, [Int])
    randoms 0 g = (g, [])
    randoms n g = let (g',  x)  = next g
                      (g'', xs) = randoms (n-1) g'
                  in (g'', x : xs)
    

    这将允许“流式传输”结果列表,因此您可以在生成后面的部分之前访问它的早期部分。 (在这种情况下,它有点微妙,因为访问结果 StdGen 必须生成整个列表,所以在你使用完列表之前,你必须小心避免这样做——我希望有一个快速支持良好 split 操作的随机生成器,那么您可以完全不必返回生成器)。

    哦,以防万一您在处理 monad 时遇到问题,这是上面用 state monad 编写的函数:

    randomsM :: Int -> State StdGen [Int]
    randomsM 0 = return []
    randomsM n = do
        x <- state next
        xs <- randomsM (n-1)
        return (x : xs)
    

    看到信件了吗?

    【讨论】:

    • 另外,randomsM = flip replicateM (state next)
    • 请注意,所有这些模式都包含在 mersenne-twister 包中。
    猜你喜欢
    • 2021-06-10
    • 2014-02-05
    • 2011-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-17
    相关资源
    最近更新 更多