未知の長さのリストからランダムな要素を選択するための2つの関数を作成しました。1つ目は(サイズ1のリザーバーを使用した)リザーバーサンプリングを使用し、2つ目はリストの長さを取得してランダムなインデックスを選択して返します。何らかの理由で、前者の方がはるかに高速です。
最初の関数は、単一の走査を使用し、確率(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
2番目のバージョンは、リストを1回トラバースしてその長さを取得し、次に0と入力リストの長さ(-1)の間のインデックスを選択して、要素の1つを同じ確率で取得します。リスト1.5の予想されるトラバーサル数:
-- Traverses the list twice
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
これら2つの関数のベンチマークに使用するコードは次のとおりです。
main :: IO ()
main = do
gen <- newStdGen
let size = 2097152
inputList = getRandList gen size
defaultMain [ bench "Using length" (pickRandomWithLen inputList)
, bench "Using reservoir" (pickRandom inputList)
]
削除された出力は次のとおりです。
benchmarking Using reservoir
mean: 82.72108 ns, lb 82.02459 ns, ub 83.61931 ns, ci 0.950
benchmarking Using length
mean: 17.12571 ms, lb 16.97026 ms, ub 17.37352 ms, ci 0.950
言い換えると、最初の関数は2番目の関数よりも約200倍高速です。ランタイムは、主に乱数の生成とリストトラバーサルの数(1対1.5)の影響を受けると予想しました。このような大きな違いを説明できる他の要因は何ですか?