性能測定のためのプログラムを書く

 では,criterionを使ってコードの性能を測定する方法を説明しましょう。例として,「昇順に整列したリスト」,「降順に整列したリスト」,「ランダムに生成されたリスト」にsort関数を適用する際の性能を測定します。性能測定のためのプログラムは以下の通りです。

import Criterion.Main
import Data.List (sort)
import System.Random

main = do
     g <- getStdGen
     defaultMain [
       bgroup "sort" [ bench "ascending"   $ nf sort generator
                     , bench "descending"  $ nf sort (reverse (generator))
                     , bench "randoms"     $ nf sort $ take (10^4) $ (randoms g::[Int])
                     ]
                   ]

generator :: [Int]
generator = [0..10^4]

 criterionでは,Criterion.MainモジュールのdefaultMain関数を使って性能を測定します。defaultMainの前に必要な前処理を書いておけば,その前処理を実行してから性能の測定を開始できます。例えば,事前にリストを生成するには,第11回で説明したusing関数とNFDataクラスを使って以下のように書きます。

import Criterion.Main
import Data.List (sort)
import System.Random

-- Require this module when using parallel 3.x.x.x.
import Control.DeepSeq
import Control.Parallel.Strategies

main = do
     g <- getStdGen
     asc <- return $! generator `using` rdeepseq
     des <- return $! (reverse asc) `using` rdeepseq
     ram <- return $! (take (10^4) (randoms g:: [Int])) `using` rdeepseq
     defaultMain [
       bgroup "sort" [ bench "ascending"   $ nf sort asc
                     , bench "descending"  $ nf sort des
                     , bench "randoms"     $ nf sort ram
                     ]
                   ]

generator :: [Int]
generator = [0..10^4]

 第11回で説明したrnfメソッドの代わりにrdeepseq関数を使っているのは,Haskell Platform 2010.2.0.0に同梱されているparallelパッケージでは少しデザインが変更されたためです。

 性能測定に使われているそれぞれの関数を見ていきましょう。

Prelude Criterion.Main> :t defaultMain
defaultMain :: [Benchmark] -> IO ()

 defaultMainでは,性能測定の対象としてBenchmark型のリストを要求します。defaultMainに渡すBenchmark型は,bench関数あるいはbgroup関数を使って作成します。

Prelude Criterion.Main> :t bench
bench :: (Benchmarkable b) => String -> b -> Benchmark
Prelude Criterion.Main> :t bgroup
bgroup :: String -> [Benchmark] -> Benchmark

 bench関数は,性能測定の対象からBenchmark型を作成するものです。測定対象の名前を示す文字列と,測定対象であるBenchmarkableクラスのインスタンスの型を取り,Benchmark型を返します。bgroup関数は,複数のBenchmarkを一つのグループとしてまとめるものです。Benchmark型のグループ名を示す文字列と,Benchmark型のリストを取り,Benchmark型を返します。

 bench関数に渡される,BenchMarkableクラスのインスタンスの型を見てみましょう。

Prelude Criterion.Main> :i Benchmarkable
class Benchmarkable a where run :: a -> Int -> IO ()
        -- Defined in Criterion.Types
instance Benchmarkable Pure -- Defined in Criterion.Types
instance Benchmarkable (IO a) -- Defined in Criterion.Types

 BenchMarkableクラスのインスタンスには,I/O処理を表現する「IO a」型と,I/Oのような副作用を伴う処理を行わない,純粋な式での評価を表現するPure型の二つがあります。Pure型は,whnf関数かnf関数で作成します。

-- | A container for a pure function to benchmark, and an argument to
-- supply to it each time it is evaluated.
data Pure where
    WHNF :: (a -> b) -> a -> Pure
    NF :: NFData b => (a -> b) -> a -> Pure

-- | Apply an argument to a function, and evaluate the result to weak
-- head normal form (WHNF).
whnf :: (a -> b) -> a -> Pure
whnf = WHNF
{-# INLINE whnf #-}

-- | Apply an argument to a function, and evaluate the result to head
-- normal form (NF).
nf :: NFData b => (a -> b) -> a -> Pure
nf = NF
{-# INLINE nf #-}

 whnfとnfの違いは,与えられた式に対する評価戦略です。whnfは「与えられた式を弱頭部正規形(WHNF)まで簡約する」というHaskellのデフォルトの戦略で評価を行います。nfは,NFDataクラスを使って,リストなどのデータ構造の中身をすべて正規系(NF)まで簡約します(WHNFとNFについては,第8回第9回で詳しく説明しています)。

instance Benchmarkable Pure where
    run p@(WHNF _ _) = go p
      where
        go fx@(WHNF f x) n
            | n <= 0    = return ()
            | otherwise = evaluate (f x) >> go fx (n-1)
    run p@(NF _ _) = go p
      where
        go fx@(NF f x) n
            | n <= 0    = return ()
            | otherwise = evaluate (rnf (f x)) >> go fx (n-1)
    {-# INLINE run #-}

 Criterion.Mainモジュールでは,whnfやnfのほかに,I/O処理を行った結果発生する値に対する評価戦略を与えるために「whnfIO」と「nfIO」も提供しています。

-- | Perform an action, then evaluate its result to head normal form.
-- This is particularly useful for forcing a lazy IO action to be
-- completely performed.
nfIO :: NFData a => IO a -> IO ()
nfIO a = evaluate . rnf =<< a
{-# INLINE nfIO #-}

-- | Perform an action, then evaluate its result to weak head normal
-- form (WHNF).  This is useful for forcing an IO action whose result
-- is an expression to be evaluated down to a more useful value.
whnfIO :: NFData a => IO a -> IO ()
whnfIO a = a >>= evaluate >> return ()
{-# INLINE whnfIO #-}

 whnfIO関数は,I/O処理の結果を強制的にWHNFに評価するものです。HaskellのIO処理は基本的には遅延評価されずに逐次的に実行されます。しかし,I/O処理の結果として作成されるデータが評価されるかどうかは保証されません。そこで,他のI/O処理と比較しやすいように,最終的なデータを評価するために使われるのがwhnfIO関数です。この関数は,データが要求されない限り実行されないforkIOのような処理の性能を測定する際にも役立ちます。

 nfIO関数は,I/O処理の結果を強制的にNFまで評価します。リスト処理の結果を返す時のように,最終的な結果をWHNFではなくNFまで簡約する必要がある場合には,whnfIOの代わりにnfIOを使います。この関数は,遅延I/Oの性能測定の際に特に役立ちます。遅延I/Oでは,要求されるデータに応じて必要なだけのI/O処理が行われます。このため,I/O処理を単に実行するだけでは,すべての処理が完了しない可能性があります。nfIOを利用すれば,遅延I/Oに対し,データをNFまで評価することで,結果を取得するのに必要なすべての処理を行うよう強制できます。