Optimization Haskell基准测试/优化非严格还原的nf/whnf

Optimization Haskell基准测试/优化非严格还原的nf/whnf,optimization,haskell,benchmarking,criterion,Optimization,Haskell,Benchmarking,Criterion,我正在尝试优化一个库,该库旨在获取大型数据集和 然后对其应用不同的操作。既然图书馆在工作,我想 优化它 我的印象是,不严格的评估允许 GHC组合操作,以便在所有操作完成时,数据只迭代一次 编写函数的类型,以便对参数进行排序,以便于whnf的缩减。 (并可能减少在每个基准上执行的操作数量) 为了测试这一点,我编写了以下代码: import Criterion.Main main = defaultMain [ bench "warmup (whnf)" $ whnf putStrL

我正在尝试优化一个库,该库旨在获取大型数据集和 然后对其应用不同的操作。既然图书馆在工作,我想 优化它

我的印象是,不严格的评估允许 GHC组合操作,以便在所有操作完成时,数据只迭代一次 编写函数的类型,以便对参数进行排序,以便于whnf的缩减。 (并可能减少在每个基准上执行的操作数量)

为了测试这一点,我编写了以下代码:

import Criterion.Main

main = defaultMain
       [ bench "warmup (whnf)" $ whnf putStrLn "HelloWorld",
         bench "single (whnf)" $ whnf single [1..10000000],
         bench "single (nf)"   $ nf   single [1..10000000],
         bench "double (whnf)" $ whnf double [1..10000000],
         bench "double (nf)"   $ nf   double [1..10000000]]

single :: [Int] -> [Int]
single lst = fmap (* 2) lst

double :: [Int] -> [Int]             
double lst =  fmap (* 3) $ fmap (* 2) lst
使用标准库进行基准测试,我得到以下结果:

benchmarking warmup (whnf)
mean: 13.72408 ns, lb 13.63687 ns, ub 13.81438 ns, ci 0.950
std dev: 455.7039 ps, lb 409.6489 ps, ub 510.8538 ps, ci 0.950

benchmarking single (whnf)
mean: 15.88809 ns, lb 15.79157 ns, ub 15.99774 ns, ci 0.950
std dev: 527.8374 ps, lb 458.6027 ps, ub 644.3497 ps, ci 0.950

benchmarking single (nf)
collecting 100 samples, 1 iterations each, in estimated 107.0255 s
mean: 195.4457 ms, lb 195.0313 ms, ub 195.9297 ms, ci 0.950
std dev: 2.299726 ms, lb 2.006414 ms, ub 2.681129 ms, ci 0.950

benchmarking double (whnf)
mean: 15.24267 ns, lb 15.17950 ns, ub 15.33299 ns, ci 0.950
std dev: 384.3045 ps, lb 288.1722 ps, ub 507.9676 ps, ci 0.950

benchmarking double (nf)
collecting 100 samples, 1 iterations each, in estimated 20.56069 s
mean: 205.3217 ms, lb 204.9625 ms, ub 205.8897 ms, ci 0.950
std dev: 2.256761 ms, lb 1.590083 ms, ub 3.324734 ms, ci 0.950
GHC是否优化了“double”功能,使列表仅为 由(*6)操作一次?nf结果表明,这种情况是因为 否则,“double”的平均计算时间将是“single”的两倍

whnf版本运行如此之快的区别是什么?我可以 仅假设没有实际执行任何操作(或仅第一次执行) (还原过程中的迭代)

我是否使用了正确的术语?

查看GHC使用
-ddump siml
选项生成的核心(中间代码),我们可以确认GHC确实将
-map
的两个应用程序融合为一个(使用
-O2
)。垃圾场的相关部分包括:

Main.main10 :: GHC.Types.Int -> GHC.Types.Int
GblId
[Arity 1
 NoCafRefs]
Main.main10 =
  \ (x_a1Ru :: GHC.Types.Int) ->
    case x_a1Ru of _ { GHC.Types.I# x1_a1vc ->
    GHC.Types.I# (GHC.Prim.*# (GHC.Prim.+# x1_a1vc 2) 3)
    }

Main.double :: [GHC.Types.Int] -> [GHC.Types.Int]
GblId
[Arity 1
 NoCafRefs
 Str: DmdType S]
Main.double =
  \ (lst_a1gF :: [GHC.Types.Int]) ->
    GHC.Base.map @ GHC.Types.Int @ GHC.Types.Int Main.main10 lst_a1gF
请注意,
Main.double
中的
GHC.Base.map
只有一个用法,指的是组合函数
Main.main10
,它既加2又乘3。这可能是GHC首先为列表内联
Functor
实例的结果,以便
fmap
成为
map
,然后允许融合
map
的两个应用程序,再加上一些内联和其他优化


WHNF表示表达式仅对“最外层”数据构造函数或lambda求值。在本例中,这意味着第一个
(:)
构造函数。这就是为什么它要快得多,因为几乎没有工作要做。有关更多详细信息,请参阅我的答案。

不是完整答案,但。。。whnf的计算结果仅足以确定结果是否为空列表。这就是为什么它这么快。吹毛求疵:
((a+2)*3)
并不等同于
(a*6)
@Mikhail,我会修正的!谢谢你的接球!(我最初写的是*3和+2 btu,当我意识到这一点时,我改变了主意)哈马尔,这太棒了!为什么我在运行-ddump siml stats时没有看到任何重写规则被使用?@Toymakerii:你在使用什么命令行?使用
ghc--make-O2-fforce recomp-ddump siml stats Bench.hs
我看到很多规则触发。没关系,我是个白痴,我的makefile没有触发,因为目标已经编译,我没有接触源代码。再次感谢,我道歉。