问题 是否有可能以不同的方式编写与“教科书”一样快的阶乘函数?


我认为这是Haskell中阶乘函数的一般形式:

factorial :: (Integral a) => a -> a
factorial n = product [1..n]

我知道这是最优雅的方式,但是当我编写自己的递归函数时,它会明显变慢:

factorial :: (Integral a) => a -> a
factorial 1 = 1
factorial n = n * factorial (n - 1)

第一个解决方案不是必须在内部完成第一个解决方案所做的一切吗?怎么这么快?是否有可能在不使用花式列表符号或产品功能的情况下编写与第一个解决方案一样快的内容?


1336
2017-11-08 04:59


起源



答案:


第一个版本比GHC更容易优化。尤其是, 产品用途 foldl

product = foldl (*) 1

并适用于 [1..n] (这是公正的 1 `enumFromTo` n它受制于 聚变。简而言之,GHC精心设计了重写规则,旨在优化中间数据结构,从而立即消耗所创建列表的代码片段(在 factorialfoldl (*) 1 是消费者和 1 `enumFromTo` n 制片人)。

请注意,你可以做GHC做的事情(factorial = foldl (*) 1 . enumFromTo 1)并获得相同的表现。


此外,你的第二个功能甚至不是尾递归。通过传递累加器,你可以很容易地解决这个问题:

factorial :: (Integral a) => a -> a
factorial n = go n 1
  where
    go 0 m = m
    go n m = go (n-1) (n*m)

与此相辅相成的是,对于大多数数字类型,您都希望算术是严格的。归结为添加 BangPatterns 至 n 和 m


10
2017-11-08 05:22



融合虽然重要,但却很多 减 在这种情况下,重要的是尾部调用优化和严格性分析的组合。 Fusion可能会使它快几倍,但其他人则不会让它完全爆炸。运用 foldl' 避免严重依赖严格性分析。 - dfeuer
@dfeuer我同意,但OP正在寻找一个理由 product 版本更快。尾调用优化是对其他版本的祝福,严格性分析适用于这两种变体。这使得融合成为主要的力量 foldl 版。 - Alec
它们的递归版本不是尾递归。将递归版本的速度与使用非融合产品的更快版本进行比较: {-# NOINLINE product_ni #-} product_ni :: [Integer] -> Integer; product_ni = product。 - dfeuer
@dfeuer哦哇,是的 - 出于某种原因,我在脑海里说它们正在使用累加器版本。非尾递归确实是一个大问题。 - Alec


答案:


第一个版本比GHC更容易优化。尤其是, 产品用途 foldl

product = foldl (*) 1

并适用于 [1..n] (这是公正的 1 `enumFromTo` n它受制于 聚变。简而言之,GHC精心设计了重写规则,旨在优化中间数据结构,从而立即消耗所创建列表的代码片段(在 factorialfoldl (*) 1 是消费者和 1 `enumFromTo` n 制片人)。

请注意,你可以做GHC做的事情(factorial = foldl (*) 1 . enumFromTo 1)并获得相同的表现。


此外,你的第二个功能甚至不是尾递归。通过传递累加器,你可以很容易地解决这个问题:

factorial :: (Integral a) => a -> a
factorial n = go n 1
  where
    go 0 m = m
    go n m = go (n-1) (n*m)

与此相辅相成的是,对于大多数数字类型,您都希望算术是严格的。归结为添加 BangPatterns 至 n 和 m


10
2017-11-08 05:22



融合虽然重要,但却很多 减 在这种情况下,重要的是尾部调用优化和严格性分析的组合。 Fusion可能会使它快几倍,但其他人则不会让它完全爆炸。运用 foldl' 避免严重依赖严格性分析。 - dfeuer
@dfeuer我同意,但OP正在寻找一个理由 product 版本更快。尾调用优化是对其他版本的祝福,严格性分析适用于这两种变体。这使得融合成为主要的力量 foldl 版。 - Alec
它们的递归版本不是尾递归。将递归版本的速度与使用非融合产品的更快版本进行比较: {-# NOINLINE product_ni #-} product_ni :: [Integer] -> Integer; product_ni = product。 - dfeuer
@dfeuer哦哇,是的 - 出于某种原因,我在脑海里说它们正在使用累加器版本。非尾递归确实是一个大问题。 - Alec


也许是这样的:

f n = foldl (*) 1 [1..n]

您可以在foldr或foldl上更改foldl'它将改变速度


2
2017-11-08 05:22