Haskell 是什么使透镜的长度无效?

Haskell 是什么使透镜的长度无效?,haskell,haskell-lens,Haskell,Haskell Lens,我在lens中注意到这一点,声称lengthOf可能“相当低效”: 它会是无症状的低效吗?尽管是严格的左折,它是否会泄漏空间,或者GHC是否会生成太多的“繁忙”代码?更新:见下文--长度的性能不佳似乎只是在基准测试时缺乏对列表案例的专门化 正如@WillemVanOnsem所指出的,我认为这个评论主要是指这样一个事实:这种特殊的方法——使用计数器遍历容器的元素——对于使用其他方法返回长度的容器来说效率低下。例如,对于非常大的向量v,从技术上讲,您可以使用导线v的长度,但是Data.vector.

我在
lens
中注意到这一点,声称
lengthOf
可能“相当低效”:

它会是无症状的低效吗?尽管是严格的左折,它是否会泄漏空间,或者GHC是否会生成太多的“繁忙”代码?

更新:见下文--长度的性能不佳似乎只是在基准测试时缺乏对列表案例的专门化

正如@WillemVanOnsem所指出的,我认为这个评论主要是指这样一个事实:这种特殊的方法——使用计数器遍历容器的元素——对于使用其他方法返回长度的容器来说效率低下。例如,对于非常大的向量
v
,从技术上讲,您可以使用
导线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
  ]
显示
长度
大约比
长度
快15倍。(我在所有测试中都使用了GHC 8.4.3和
-O2

请注意,这种差异不是列表融合的结果(因为在使用
whnf
调用时,
Prelude.length
案例中没有融合)

它实际上是将代码专门化为列表的结果。尽管
Prelude.length
适用于任何
可折叠的
,但列表实例使用特定于列表的实现,该实现本质上等同于:

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)
它生成了如下所示的核心。与上面相同,还有一个额外的参数,它本质上是一个强制转换标识函数:

lenAcc'
lenAcc'
  = \n id' xs ->
      case xs of {
        [] -> id' (I# n);
        (:) _ xs' -> lenAcc' (+# n 1#) id' xs'
      }
我无法对它进行基准测试,但它可能会非常快

因此,
lengthOf traverse
可以优化到几乎与
Prelude.length
一样快,但这取决于它的使用方式,它可能会使用一个非常低效的实现。

更新:见下文--的
lengthOf的低性能似乎只是在基准测试时缺乏对列表案例的专门化

正如@WillemVanOnsem所指出的,我认为这个评论主要是指这样一个事实:这种特殊的方法——使用计数器遍历容器的元素——对于使用其他方法返回长度的容器来说效率低下。例如,对于非常大的向量
v
,从技术上讲,您可以使用
导线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
  ]
显示
长度
大约比
长度
快15倍。(我在所有测试中都使用了GHC 8.4.3和
-O2

请注意,这种差异不是列表融合的结果(因为在使用
whnf
调用时,
Prelude.length
案例中没有融合)

它实际上是将代码专门化为列表的结果。尽管
Prelude.length
适用于任何
可折叠的
,但列表实例使用特定于列表的实现,该实现本质上等同于:

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)
它生成了如下所示的核心。与上面相同,还有一个额外的参数,它本质上是一个强制转换标识函数:

lenAcc'
lenAcc'
  = \n id' xs ->
      case xs of {
        [] -> id' (I# n);
        (:) _ xs' -> lenAcc' (+# n 1#) id' xs'
      }
我无法对它进行基准测试,但它可能会非常快

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

它在线性时间内运行,计算长度通常相当长。它在线性时间内运行,这通常是相当长的计算长度。