gpt4 book ai didi

haskell - 如何从多次运行的 haskell 基准测试中获取更有意义的统计数据

转载 作者:行者123 更新时间:2023-12-04 02:28:28 25 4
gpt4 key购买 nike

我正在使用 benchpress 库运行一些相当简单的基准测试。我一直在使用 bench::Int -> IO a -> IO () 接口(interface)。但是,似乎如果我运行给定函数 n 次,第一次之后的所有运行都非常快。

举个简单的例子,bench 1 (seq (sum [1..100000]) (return ())) 可能需要 10 秒左右。但是,bench 5 (seq (sum [1..100000]) (return ())) 将生成如下报告:

Times (ms)
min mean +/-sd median max
0.001 2.657 5.937 0.001 13.277

Percentiles (ms)
50% 0.001
66% 0.002
75% 0.002
80% 0.002
90% 13.277
95% 13.277
98% 13.277
99% 13.277
100% 13.277

因为平均值是 2.6,我可以推断出第一次运行用了 13 秒,而其他 4 次非常快。

为什么会这样?我如何确保基准测试的所有运行都具有代表性?该库还有一个更细粒度的接口(interface):benchmark::Int -> IO a -> (a -> IO b) -> (a -> IO c) -> IO (Stats, Stats)。这将使我能够提供设置和拆卸功能——我可以使用这个界面来获得更有意义的结果吗?

最佳答案

我建议使用 criterion。它经过精心设计,具有用于计算纯计算时间的工具(正如您所发现的,这可能很棘手)。我不熟悉 benchpress,但它似乎没有开箱即用的相同功能,而且似乎主要旨在对 IO 操作进行基准测试。

criterion 中对您的示例进行基准测试看起来像这样:

import Criterion.Main

main = defaultMain
[ bench "my summation" $ whnf sum [1..100000] ]

从没有优化标志的 GHCi 和 ghc 运行的基准测试在很大程度上是没有意义的,因此使用 ghc -O2 编译它很重要。运行它将产生输出:

benchmarking my summation
time 9.393 ms (9.271 ms .. 9.498 ms)
0.998 R² (0.997 R² .. 0.999 R²)
mean 9.385 ms (9.292 ms .. 9.483 ms)
std dev 268.7 μs (208.4 μs .. 334.0 μs)

您可以在此处看到时间从最小值 9.3 毫秒到 9.5 毫秒不等,因此没有大的异常值。但是,Criterion 会自动放弃初始运行,以确保仅在第一次运行代码时产生的成本(GHC 代码经常发生)不会包含在计时中。

whnf 函数是一个神奇的函数,它确保即使它的两个参数可能在第一次运行后被完全求值并因此在内存中完全形成,它的第一个参数应用到它的第二个将真正重复每次运行,并且评估将进行得足够远以将结果置于“弱头正常形式”中。一个数的弱头范式(比如一堆整数的和)就是这个数本身,所以对于这个基准测试,时机是对实际数值和的评估。

重要的是要了解此计算的哪些部分没有被基准化。表达式 [1..100000] 构造一个列表。如果列表没有被优化掉(在这个基准测试中它没有),列表的构造,作为一个完全保存在内存中的装箱 Integer 的单链表,执行第一个被丢弃的迭代,这里的基准时间是遍历构造列表以求和其元素。您可以对列表的构造和求和进行计时:

bench "construct and sum" $ whnf (\n -> sum [1..n]) 100000

但这会产生出乎意料的更快的结果:

benchmarking construct and sum
time 1.299 ms (1.288 ms .. 1.314 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 1.290 ms (1.285 ms .. 1.297 ms)
std dev 20.77 μs (14.74 μs .. 27.59 μs)

因为列表通过列表融合进行了优化,您现在正在对一个紧密的求和循环进行基准测试。

如果你真的想计时一个显式列表的构造和求和,你可以用一个不内联的 sum 的副本来防止列表融合:

sum' :: (Num a) => [a] -> a
{-# NOINLINE sum' #-}
sum' = sum

...bench "construct and sum w/o fusion" $ whnf (\n -> sum' [1..n]) 100000...

也就是说,对 GHC 代码进行基准测试很棘手,但使用 criterion 几乎是强制性的。

一个完整的例子:

import Criterion.Main

{-# NOINLINE sum' #-}
sum' :: (Num a) => [a] -> a
sum' = sum

main = defaultMain
[ bench "sum an in-memory list" $ whnf sum [1..100000]
, bench "construct and sum w/ fusion" $ whnf (\n -> sum [1..n]) 100000
, bench "construct and sum w/o fusion" $ whnf (\n -> sum' [1..n]) 100000
, bench "Int (vs. Integer) and fusion" $ whnf (\n -> sum[(1::Int)..n]) 100000
]

我使用 ghc -O2 得到的时间大致是 9ms、1ms、14ms 和 47μs。请注意,与 Integer 相比,Int 非常快,如果您没有使用显式类型签名并且无意中默认为 Integer .

在这里,差异与数据类型本身关系不大,而与拆箱和融合的组合关系更大。最终基准被编译成一个相当紧凑的汇编循环,将寄存器中的数字从 1 添加到 100000。

实际上, native 代码生成器在这里做得不好。 LLVM 后端 (ghc -O2 -fllvm) 将 Int 版本缩短到 100 纳秒。当你得到这么小的时间时,最好扩大问题的规模,以确保你实际上在测量你认为你在测量的东西。如果我将列表长度扩大 10 倍,则所有时间都扩大 10 倍,因此我可以有理由相信我正在按预期对实际求和进行计时。

关于haskell - 如何从多次运行的 haskell 基准测试中获取更有意义的统计数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65742862/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com