Performance 水库采样与获取列表长度和随机元素的性能对比

Performance 水库采样与获取列表长度和随机元素的性能对比,performance,haskell,criterion,Performance,Haskell,Criterion,我已经编写了两个函数从未知长度的列表中选择一个随机元素。第一种方法使用库采样(库大小为1),第二种方法获取列表的长度,以选择一个随机索引并返回它。出于某种原因,前者要快得多 第一个函数使用单个遍历并以概率(1/i)拾取每个元素,其中i是列表中元素的索引。它导致选择每个元素的概率相等 pickRandom :: [a] -> IO a pickRandom [] = error "List is empty" pickRandom (x:xs) = do stdgen <- new

我已经编写了两个函数从未知长度的列表中选择一个随机元素。第一种方法使用库采样(库大小为1),第二种方法获取列表的长度,以选择一个随机索引并返回它。出于某种原因,前者要快得多

第一个函数使用单个遍历并以概率(1/i)拾取每个元素,其中i是列表中元素的索引。它导致选择每个元素的概率相等

pickRandom :: [a] -> IO a
pickRandom [] = error "List is empty"
pickRandom (x:xs) = do
  stdgen <- newStdGen
  return (pickRandom' xs x 1 stdgen)

-- Pick a random number using reservoir sampling
pickRandom' :: (RandomGen g) => [a] -> a -> Int -> g -> a
pickRandom' [] xi _ _ = xi
pickRandom' (x:xs) xi n gen =
  let (rand, gen') = randomR (0, n) gen in
  if (rand == 0) then
    pickRandom' xs x (n + 1) gen' -- Update value
  else
    pickRandom' xs xi (n + 1) gen' -- Keep previous value

换句话说,第一个函数比第二个函数快大约200倍。我预计运行时主要受随机数生成和列表遍历次数(1对1.5)的影响。还有什么其他因素可以解释如此巨大的差异呢?

你的基准行动实际上并没有评估结果

pickRandom :: [a] -> IO a
pickRandom [] = error "List is empty"
pickRandom (x:xs) = do
  stdgen <- newStdGen
  return (pickRandom' xs x 1 stdgen)
return $! ...
计算列表的长度,然后返回thunk,这当然要慢得多

迫使双方评估结果

pickRandom :: [a] -> IO a
pickRandom [] = error "List is empty"
pickRandom (x:xs) = do
  stdgen <- newStdGen
  return (pickRandom' xs x 1 stdgen)
return $! ...
使使用版本的
长度
更快

使用长度进行基准测试
平均值:14.65655毫秒,磅14.14580毫秒,ub 15.16942毫秒,置信区间0.950
标准偏差:2.631668毫秒,磅2.378186毫秒,ub 2.937339毫秒,置信区间0.950
异常值引入的方差:92.581%
异常值严重夸大了方差
利用水库进行基准测试
在估计的47.00930秒内收集100个样本,每个样本重复1次
平均值:451.5571毫秒,磅448.4355毫秒,ub 455.7812毫秒,置信区间0.950
标准偏差:18.50427毫秒,磅14.45557毫秒,ub 24.74350毫秒,ci 0.950
在100个样本中发现4个异常值(4.0%)
2(2.0%)轻度偏高
2(2.0%)高度严重
异常值引入的方差:38.511%
异常值适度夸大了方差
(在通过打印输入列表的总和强制输入列表之前进行评估之后),因为这只需要一次PRNG调用,而水库采样使用
长度列表-1
调用

如果使用比StdGen更快的PRNG,则差异可能会更小

实际上,使用而不是
StdGen
(要求
pickRandom'
具有
IO a
结果类型,并且由于它在特定范围内不提供生成,但仅提供默认范围,因此拾取元素的分布会略微倾斜,但由于我们只关心生成伪随机数所需的时间,这并不重要),水库取样时间降至

平均值:51.83185毫秒,磅51.77620毫秒,ub 51.91259毫秒,置信区间0.950
标准偏差:482.4712美国,磅368.4433美国,ub 649.1758美国,ci 0.950
(当然,
pickRandomWithLen
时间不会发生可测量的变化,因为它只使用一代)。大约九倍的加速,这表明伪随机生成是主要因素

pickRandomWithLen :: [a] -> IO a
pickRandomWithLen [] = error "List is empty"
pickRandomWithLen xs = do
  gen <- newStdGen
  (e, _) <- return $ randomR (0, (length xs) - 1) gen
  return $ xs !! e
return $! ...