是什么使lengthOf的镜片效率低下?

时间:2018-07-28 13:20:31

标签: haskell lens

我在lens中注意到了这个remark,声称lengthOf可能是“效率低下的”:

-- /Note:/ This can be rather inefficient for large containers [...]
lengthOf :: Getting (Endo (Endo Int)) s a -> s -> Int
lengthOf l = foldlOf' l (\a _ -> a + 1) 0

它可以渐近地效率低下吗?尽管被严格折左,它会泄漏空间吗?还是GHC会生成过多的“忙碌工作”代码?

1 个答案:

答案 0 :(得分:2)

更新:参见下文-基准测试时,lengthOf的糟糕表现似乎只是缺乏专业化。

正如@WillemVanOnsem指出的那样,我认为评论主要是指以下事实:对于使用其他方法返回容器的容器来说,这种特殊的方法(使用计数器运行容器的元素)将是无效的。长度。例如,对于非常大的向量v,您可以从技术上使用lengthOf traverse v,但是Data.Vector.length v会更快。

另一方面,lengthOf 对于计数列表中的元素可能效率很低。以下基准:

import Criterion.Main
import Control.Lens

l :: [Int]
l = replicate 1000000 123

main = defaultMain
  [ bench "Prelude.length"        $ whnf length l
  , bench "Control.Lens.lengthOf" $ whnf (lengthOf traverse) l
  ]

显示lengthlengthOf快15倍。 (我在所有测试中都将GHC 8.4.3与-O2一起使用。)

请注意,这种差异不是列表融合的结果(因为在使用Prelude.length的情况下,whnf中没有融合)。

这实际上是代码对列表进行专业化的结果。即使Prelude.length适用于任何Foldable,列表的实例也使用特定于列表的实现,该实现基本上等同于:

myLength :: [a] -> Int
myLength xs = lenAcc xs 0
  where lenAcc [] n = n
        lenAcc (_:ys) n = lenAcc ys (n+1)

(我没有确定这是否正在使用该实现,但是myLength的性能几乎与Data.List相当。)

myLength的核心在直接与列表构造函数模式匹配的循环中使用未装箱的整数,或多或少像这样:

lenAcc
  = \xs n ->
      case xs of
        [] -> n
        (:) _ xs' -> lenAcc xs' (+# n 1#)

事实证明,如果我在更现实的程序中使用lengthOf,并且有足够的空间以相同的方式专门研究列表:

import Control.Lens

l :: [Int]
{-# NOINLINE l #-}
l = replicate 1000000 123

myLength :: [a] -> Int
myLength = lengthOf traverse

main = print (myLength l)

它生成了如下的Core。与上面相同,但具有一个额外的参数,该参数本质上是转换身份函数:

lenAcc'
lenAcc'
  = \n id' xs ->
      case xs of {
        [] -> id' (I# n);
        (:) _ xs' -> lenAcc' (+# n 1#) id' xs'
      }

我无法对其进行基准测试,但可能很快。

因此,lengthOf traverse可以被优化为几乎与Prelude.length一样快,但是取决于其使用方式,它最终可能会使用效率非常低的实现。