Как можно форсировать оценку одного и того же значения несколько раз в Haskell? - PullRequest
4 голосов
/ 29 марта 2020

У меня есть функция bench, которую можно использовать для вычисления времени, необходимого для оценки action:

data Benchmark
  = Benchmark POSIXTime POSIXTime
  | BenchmarkN [Benchmark]

bench :: a -> IO Benchmark
bench action
  = do
    start  <- getPOSIXTime
    let !_ = action
    end    <- getPOSIXTime
    return $ Benchmark start end

Я пытаюсь взять среднее из нескольких эталонов для указанного action, однако последующие оценки action происходят почти мгновенно, поскольку он уже был оценен один раз:

benchN :: Int -> a -> IO Benchmark
benchN count action
  = BenchmarkN <$> (mapM bench $ replicate count action)

В любом случае можно ли принудительно вычислять action несколько раз, так что это займет полное время оценивать?

Ссылка на репо: https://github.com/wdhg/benchy

1 Ответ

1 голос
/ 04 апреля 2020

Техника, которую использует criterion, состоит в том, чтобы скомпилировать функцию whnf' в ее собственном модуле без встраивания и специального -fno-full-laziness флага оптимизации, что-то вроде:

-- WHNF.hs
{-# OPTIONS_GHC -fno-full-laziness #-}

module WHNF (whnf') where

whnf' :: (a -> b) -> a -> (Int -> IO ())
whnf' f x = go
  where
    go n | n <= 0 = return ()
         | otherwise = f x `seq` go (n-1)
{-# NOINLINE whnf' #-}

Здесь вычисление представляется в двух частях - как функция и аргумент для вызова. Функция whnf' превращает ее в эталонную функцию Int -> IO (), которая берет счетчик репликации и будет безопасно перезапускать вычисления (в частности, приводя их к нормальной форме со слабой головой) указанное количество раз.

Обратите внимание, что счетчик репликации здесь не для генерации нескольких отдельных таймингов. Скорее, он используется для увеличения времени, когда бенчмаркинг действительно быстрых вычислений, так что временные затраты не влияют на эталонный тест. Для медленных вычислений вы можете использовать счетчик 1.

В вашем основном модуле сравнительного анализа вам, как правило, также необходимо представить выражение, которое нужно сравнить с использованием тех же двух частей, функция и аргумент, чтобы вызвать его. Хотя это и не обязательно, может быть удобно ввести тип данных для этого, включая шкалу подсчета репликации:

data Benchmarkable a b = Benchmarkable (a -> b) a Int

, а затем вы можете сравнить ее один раз с:

data Benchmark
  = Benchmark POSIXTime POSIXTime
  | BenchmarkN [Benchmark]
  deriving (Show)

bench :: Benchmarkable a b -> IO Benchmark
bench (Benchmarkable f a n) = do
  start  <- getPOSIXTime
  () <- whnf' f a n
  end    <- getPOSIXTime
  return $ Benchmark start end

или несколько раз:

benchN :: Int -> Benchmarkable a b -> IO Benchmark
benchN count b = BenchmarkN <$> replicateM count (bench b)

Если у вас медленная реализация Фибоначчи:

slowFib :: Integer -> Integer
slowFib 0 = 0
slowFib 1 = 1
slowFib n = slowFib (n-1) + slowFib (n-2)

, где slowFib 35 занимает значительную долю секунды для запуска, вы можете попробовать :

main = print =<< benchN 10 (Benchmarkable slowFib 35 1)

и, кажется, работает нормально, выводит:

BenchmarkN [Benchmark 1586018307.738716168s 1586018308.179642319s,
Benchmark 1586018308.179642466s 1586018308.618854568s,
Benchmark 1586018308.618854653s 1586018309.057612242s,
Benchmark 1586018309.057612287s 1586018309.496228626s,
Benchmark 1586018309.496228714s 1586018309.934910649s,
Benchmark 1586018309.934910697s 1586018310.373258208s,
Benchmark 1586018310.373258295s 1586018310.811727495s,
Benchmark 1586018310.811727542s 1586018311.250130875s,
Benchmark 1586018311.250131005s 1586018311.689046116s,
Benchmark 1586018311.689046207s 1586018312.127901112s]

Полный код для модуля WHNF:

-- WHNF.hs
{-# OPTIONS_GHC -fno-full-laziness #-}

module WHNF (whnf') where

whnf' :: (a -> b) -> a -> (Int -> IO ())
whnf' f x = go
  where
    go n | n <= 0 = return ()
         | otherwise = f x `seq` go (n-1)
{-# NOINLINE whnf' #-}

и сам тест в отдельный модуль:

-- Benchmark.hs
{-# OPTIONS_GHC -O2 #-}

import WHNF
import Data.Time.Clock.POSIX
import Control.Monad

data Benchmarkable a b = Benchmarkable (a -> b) a Int

data Benchmark
  = Benchmark POSIXTime POSIXTime
  | BenchmarkN [Benchmark]
  deriving (Show)

bench :: Benchmarkable a b -> IO Benchmark
bench (Benchmarkable f a n) = do
  start  <- getPOSIXTime
  () <- whnf' f a n
  end    <- getPOSIXTime
  return $ Benchmark start end

benchN :: Int -> Benchmarkable a b -> IO Benchmark
benchN count b = BenchmarkN <$> replicateM count (bench b)

slowFib :: Integer -> Integer
slowFib 0 = 0
slowFib 1 = 1
slowFib n = slowFib (n-1) + slowFib (n-2)

main :: IO ()
main = print =<< benchN 10 (Benchmarkable slowFib 35 1)
...