在Code Review上,我回答了一个关于naive Haskell fizzbuzz solution的问题,提出了iterates forward的实现,避免了越来越多的素数的二次成本,并且几乎完全丢弃了模数除法。这是代码:
fizz :: Int -> String
fizz = const "fizz"
buzz :: Int -> String
buzz = const "buzz"
fizzbuzz :: Int -> String
fizzbuzz = const "fizzbuzz"
fizzbuzzFuncs = cycle [show, show, fizz, show, buzz, fizz, show, show, fizz, buzz, show, fizz, show, show, fizzbuzz]
toFizzBuzz :: Int -> Int -> [String]
toFizzBuzz start count =
let offsetFuncs = drop (mod (start - 1) 15) fizzbuzzFuncs
in take count $ zipWith ($) offsetFuncs [start..]
作为进一步的提示,我建议使用Data.List.unfoldr
重写它。 unfoldr
版本是对此代码的一个明显,简单的修改,所以我不打算在这里输入它,除非那些寻求回答我的问题的人坚持认为这很重要(OP代码审查没有破坏者)。但我对unfoldr
解决方案与zipWith
解决方案的相对效率存在疑问。虽然我不再是Haskell的新手,但我并不是Haskell内部的专家。
unfoldr
解决方案不需要[start..]
无限列表,因为它可以从start
展开。我的想法是
zipWith
解决方案不会按要求记住[start..]
的每个连续元素。使用和丢弃每个元素,因为不保留对[start ..]的头部的引用。因此,与unfoldr
相比,没有更多的内存消耗。unfoldr
和最近补丁的表现的担忧,使其始终内联,是在我尚未达到的水平上进行的。所以我认为这两者在内存消耗方面相当,但不了解相对性能。希望更有信息的Haskellers能指导我理解这一点。
unfoldr
似乎很自然地用于生成序列,即使其他解决方案更具表现力。我只知道我需要更多地了解它的实际性能。 (出于某种原因,我发现foldr
更容易在该级别上理解)
注意:unfoldr
使用Maybe
是我在开始调查问题之前发生的第一个潜在的性能问题(并且是我完全理解的优化/内联讨论。所以我能够立即停止担心Maybe
(鉴于最新版本的Haskell)。
答案 0 :(得分:10)
作为负责最近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
库中使用)的想法是,如果列表是由“好的制作者”制作的,那么“好的制作者”会改变变形金刚“,并由”好消费者“消费,然后列表根本不需要实际分配。旧unfoldr
不是一个优秀的制作人,所以如果您制作了一个包含unfoldr
的列表,并将其与foldr
一起使用,列表中的各个部分随着计算的进行,将被分配(并立即变成垃圾)。现在,unfoldr
是一个很好的生产者,所以你可以使用unfoldr
,filter
和foldr
编写一个循环,而不是(必然)分配任何内存。所有
例如,鉴于上面定义的bloob
和一个严格的{-# 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
,没有对;它执行的唯一分配是用于存储最终结果的Int
(I#
到ww2_s1oT
的应用)。可以合理地期望整个计算在机器寄存器中执行。
zipWith
zipWith
有一个奇怪的故事。它有点笨拙地适应折叠/构建框架(我相信它在流融合方面效果更好)。有可能使zipWith
与其第一个或第二个列表参数融合,并且多年来,列表库试图使它与任何一个融合,如果它们是一个好的生产者。不幸的是,在某些情况下,使其与第二个列表参数融合可以使程序更少定义。也就是说,使用zipWith
的程序在没有优化的情况下编译时可以正常工作,但在使用优化编译时会产生错误。这不是一个很好的情况。因此,从base-4.8
开始,zipWith
不再尝试与其第二个列表参数融合。如果你想让它与一个优秀的制作人融合,那么优秀的制作人最好是在第一个列表参数中。
具体而言,zipWith
的参考实施会导致期望zipWith (+) [1,2,3] (1 : 2 : 3 : undefined)
会给[2,4,6]
,因为它会在到达第一个列表的末尾时立即停止。使用之前的zipWith
实现,如果第二个列表看起来像那样但是由一个好的制作人制作,并且如果zipWith
碰巧与它融合而不是第一个列表,那么它将会繁荣。 / p>