作为负责 and 的最近实现变化的人zipWith
,unfoldr
我想我可能应该尝试一下。我不能那么容易地比较它们,因为它们是非常不同的功能,但我可以尝试解释它们的一些属性和变化的意义。
unfoldr
内联
旧版本unfoldr
(在base-4.8
/GHC 7.10 之前)在顶层是递归的(它直接调用自身)。GHC 从不内联递归函数,因此unfoldr
从未内联。结果,GHC 无法看到它如何与传递的函数进行交互。最令人不安的影响是传入的函数,类型(b -> Maybe (a, b))
,实际上会产生Maybe (a, b)
值,分配内存来保存Just
和(,)
构造函数。通过重组unfoldr
为“worker”和“wrapper”,新代码允许 GHC 内联它并(在许多情况下)将其与传入的函数融合,因此额外的构造函数被编译器优化剥离。
例如,在 GHC 7.10 下,代码
module Blob where
import Data.List
bloob :: Int -> [Int]
bloob k = unfoldr go 0 where
go n | n == k = Nothing
| otherwise = Just (n * 2, n+1)
编译与ghc -O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures
导致核心
$wbloob :: Int# -> [Int]
$wbloob =
\ (ww_sYv :: Int#) ->
letrec {
$wgo_sYr :: Int# -> [Int]
$wgo_sYr =
\ (ww1_sYp :: Int#) ->
case tagToEnum# (==# ww1_sYp ww_sYv) of _ {
False -> : (I# (*# ww1_sYp 2)) ($wgo_sYr (+# ww1_sYp 1));
True -> []
}; } in
$wgo_sYr 0
bloob :: Int -> [Int]
bloob =
\ (w_sYs :: Int) ->
case w_sYs of _ { I# ww1_sYv -> $wbloob ww1_sYv }
融合
另一个更改unfoldr
是重写它以参与“折叠/构建”融合,这是 GHC 列表库中使用的优化框架。“折叠/构建”融合和更新的、不同平衡的“流融合”(在vector
库中使用)的想法是,如果列表由“好的生产者”生成,由“好的转换器”转换,然后被消费由一个“好消费者”,那么列表 conses 根本不需要分配。oldunfoldr
不是一个好的生产者,所以如果你生成一个列表并unfoldr
用它来消费它,比如说,foldr
列表的各个部分将在计算进行时被分配(并立即变成垃圾)。现在,unfoldr
是一个很好的生产者,所以你可以写一个循环,比如说,unfoldr
filter
foldr
,而不是(必然)分配任何内存。
例如,给定上面的bloob
, 和一个 stern的定义{-# INLINE bloob #-}
(这东西有点脆弱;好的生产者有时需要显式内联才能很好),代码
hooby :: Int -> Int
hooby = sum . bloob
编译到 GHC 核心
$whooby :: Int# -> Int#
$whooby =
\ (ww_s1oP :: Int#) ->
letrec {
$wgo_s1oL :: Int# -> Int# -> Int#
$wgo_s1oL =
\ (ww1_s1oC :: Int#) (ww2_s1oG :: Int#) ->
case tagToEnum# (==# ww1_s1oC ww_s1oP) of _ {
False -> $wgo_s1oL (+# ww1_s1oC 1) (+# ww2_s1oG (*# ww1_s1oC 2));
True -> ww2_s1oG
}; } in
$wgo_s1oL 0 0
hooby :: Int -> Int
hooby =
\ (w_s1oM :: Int) ->
case w_s1oM of _ { I# ww1_s1oP ->
case $whooby ww1_s1oP of ww2_s1oT { __DEFAULT -> I# ww2_s1oT }
}
它没有列表,没有Maybe
s,也没有对;它执行的唯一分配是Int
用于存储最终结果(to 的应用程序I#
)ww2_s1oT
。可以合理地预期整个计算将在机器寄存器中执行。
zipWith
zipWith
有一个奇怪的故事。它有点笨拙地适合折叠/构建框架(我相信它在流融合方面工作得更好)。可以与它的第一个或第二个列表参数进行zipWith
融合,并且多年来,列表库试图使其与其中任何一个融合,如果其中一个是一个好的生产者的话。不幸的是,使其与第二个列表参数融合可能会使程序在某些情况下的定义更少。也就是说,一个程序 usingzipWith
在没有优化的情况下编译时可以正常工作,但在使用优化编译时会产生错误。这不是一个很好的情况。因此,截至base-4.8
,zipWith
不再尝试与它的第二个列表参数融合。如果你想让它与一个好的生产者融合,那个好的生产者最好在第一个列表参数中。
具体来说, 的参考实现zipWith
会导致预期zipWith (+) [1,2,3] (1 : 2 : 3 : undefined)
会给出[2,4,6]
,因为它会在到达第一个列表的末尾时立即停止。在之前的zipWith
实现中,如果第二个列表看起来像这样,但是是由一个好的生产者制作的,并且如果zipWith
碰巧与它融合而不是第一个列表,那么它就会繁荣起来。