性能測定のためのプログラムを書く
では,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まで評価することで,結果を取得するのに必要なすべての処理を行うよう強制できます。