Haskell无意义的性能 - 有效地将多个函数映射到相同的数据

时间:2013-04-29 16:10:06

标签: haskell functional-programming pointfree

我经常要将多个功能映射到相同的数据。我已经实现了dpMap来为我做这个

dpMap fns = (`map` fns) . flip ($)

dpMap是一个函数,这是否意味着我只读了一次数据dt(就像一个输入相同输入的电路。一个无意义的系统提醒一个电路;只是管道没有数据)?

作为一个例子,考虑计算列表的最小值和最大值。

minimax dt = (dpMap [minimum, maximum]) dt

(我可以摆脱dt但必须使用-XNoMonomorphismRestriction)

在这样的点完全形式中实现相同的功能是否有性能优势?:

minimax2 dt = [minimum dt, maximum dt]
编辑:dpMap的常规实现是否适用于常量内存?

我找到了另一篇不错的博文:http://www.haskellforall.com/2013/08/composable-streaming-folds.html;希望这有帮助。

<击> 编辑2:经过一些更多的上下文,这里有一个解决方案,即使我没有精确的dpMap实现,模式很简单,它不保证单独的功能:

minimax = (,) <$> minimum <*> maximum

用法:

> minimax [1..100]
(1,100)

如果您还想计算总和和长度

func = (,,,) <$> minimum <*> maximum <*> sum <*> length

用法:

> func [1..100]
(1,100,5050,100)

<击>

2 个答案:

答案 0 :(得分:9)

TL; DR :对语言本身的性能无法保证。没有任何。它是编译器的东西。

根据经验,名为的实体将驻留在内存中。如果仅由一个消费者访问 lazily ,则期望对其进行优化以使编译的程序在常量内存中运行是合理的。

存储器单元的创建和使用将被交错,每个单元在处理后都将进行GC编辑。


minimax2 dt = [minimum dt, maximum dt]中,表达式[minimum dt, maximum dt]位于定义命名实体dt的范围内。很可能(即几乎可以肯定)GHC将其分配为一个内存实体,即一次,并且表达式中的dt都将引用同一个实体(指向它,就像指针一样)。

但正如Cat Plus Plus在评论中指出的那样,当然如何访问实体是另一回事。并且两个子表达式将分别访问它,即它将完全保留在存储器中。这不好。

我们可以做得更好,找到我们的答案只需访问一次,然后折叠,随着我们的进展收集两个数据。在这种情况下,几乎可以肯定GHC将执行优化,此列表将保留在整个内存中。

这通常被称为 lazily 消耗的列表。在这种情况下,它的创建将与该一次访问交错,并且通过GC(垃圾收集)立即消耗和释放所生成的每个存储器单元,从而实现恒定的存储器操作。

但这取决于我们只扫描列表一次的能力:

{-# OPTIONS_GHC -O2 -XBangPatterns #-}

import Data.List (foldl')

minmax :: (Ord b) => [b] -> (b, b)
minmax (x:xs) = foldl' (\(!a,!b) x -> (min a x,max b x)) (x,x) xs

Bang模式可防止thunk积聚,使得对参数的评估更加迫切。测试:

Prelude> minmax [1..6]
(1,6)
Prelude> minmax []
*** Exception: <interactive>:1:4-65: Non-exhaustive patterns in function minmax

当然空列表没有定义最小值或最大值。

要进行优化,在使用GHC进行编译时必须使用-O2标志。

答案 1 :(得分:3)

我将在这个答案中对这个问题采取相当广泛的看法,主要是因为WillNess答案下的评论。

blog post中,Max Rabkin介绍了折叠组合器的一些工作。 Conal Elliott接受了这个想法并发布了一些博客文章以及关于hackage的ZipFold package。我强烈建议您阅读这些材料,它很简短,非常方便。 ZipFold软件包可能非常有用,虽然它已经有一段时间没有更新了。

爱德华·凯梅特最近的巡回演出lens也包括一些folding combinators。我不确定我是否只想使用它,但如果你还在使用镜头那么它可能值得一试。

另一种方法是使用并行性。如果你写

import Control.Parallel

minimax2 dt = let a = minimum dt
                  b = maximum dt
              in a `par` b `pseq` [a,b]

并与-threaded链接,然后minimax2可能在接近恒定空间的某些地方运行,具体取决于调度程序的变幻无常,月亮的阶段等(主要是调度程序和分配模式)职能IIRC)。当然,这并不能提供可靠的保证,但它在实践中可以很好地工作。将此方法概括为dpMap应该很简单,您可能希望使用Control.Parallel.Strategies或类似方法,而不是直接使用较低级别的par

最后,大多数迭代派生的库都非常擅长处理这类任务。通常,它们提供对输入流何时产生和消耗的明确控制。在iteratee我提供的sequence_dpMap几乎相同,将添加与sequence完全相同的dpMap和一个数字拉链,所有这些都在恒定的空间中运行(假设消耗函数本身是恒定空间)。如果大多数其他软件包提供类似的操作,我不会感到惊讶。