我在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会生成过多的“忙碌工作”代码?
答案 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
]
显示length
比lengthOf
快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
一样快,但是取决于其使用方式,它最终可能会使用效率非常低的实现。