packages feed

tasty-papi (empty) → 0.1

raw patch · 6 files changed

+854/−0 lines, 6 filesdep +basedep +containersdep +deepseq

Dependencies added: base, containers, deepseq, stm, tasty, tasty-papi

Files

+ Changelog.md view
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) Alexey Khudyakov++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of his contributors+   may be used to endorse or promote products derived from this software+   without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,13 @@+# tasti-papi++This is another approach to benchmarking which uses counting of CPU instructions+instead of time measurements. Underlying hardware counters are accessed using+[PAPI](https://icl.utk.edu/papi/) library so all hardware and OS support is+inherited from there.++This approach is suitable for benchmarking CPU bound code. Instruction count is+only proxy for code performance but it has advantage of being+deterministic. Timing measurements are affected by concurrent workloads and require+dedicated hardware in order to get precise measurements. Instruction counting is+not affected by this so it could be run as part of CI and used to detect+performance regressions.
+ Test/Tasty/PAPI.hs view
@@ -0,0 +1,722 @@+{-# LANGUAGE CApiFFI                    #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE ForeignFunctionInterface   #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase                 #-}+{-# LANGUAGE TypeApplications           #-}+{- |+Module:      Test.Tasty.PAPI+Copyright:   (c) 2023 Alexey Khudyakov+Licence:     BSD3++Benchmark framework which uses CPU instruction counting instead of+time measurement. This approach is much more deterministic and not+subject to variation caused by concurrent execution of other+programs.++Hardware counters are accessedusing+[PAPI](https://icl.utk.edu/papi/). Thus OS and hardware support+inherited from that library.+++=== How to use++Library uses standard approach for benchmarks. So example benchmarks+looks similar to one using @criterion@, @gauge@ or @test-bench@:++> module Main where+> import Test.Tasty.PAPI+> +> main :: IO ()+> main = defaultMain+>   [ bench "6" $ whnf fib 6+>   , bench "7" $ whnf fib 7+>   , bench "8" $ whnf fib 8+>   ]+> +> fib :: Integer -> Integer+> fib 0 = 0+> fib 1 = 1+> fib n = fib (n-1) + fib (n-2)++Its output is:++> All+>   6:                OK+>     ALLOC=528  TOT_INS=4768    TOT_CYC=6114    BR_INS=1128     BR_MSP=74+>   7:                OK+>     ALLOC=864  TOT_INS=7431    TOT_CYC=6631    BR_INS=1744     BR_MSP=70+>   8:                OK+>     ALLOC=1408 TOT_INS=11.75e3 TOT_CYC=8540    BR_INS=2743     BR_MSP=93+++=== Command line optiosn++Use @--help@ to list command-line options. Below is list of options+provided by this package:++[@--csv@ @PATH@]++    Write benchmark results into file in CSV format.++[@--counters@ @COUNTER_SET@]++    Adjust set of hardware counters to use. Refer to 'Counter' for+    documentation on supported counters. By default 'TOT_INS', 'TOT_CYC',+    'BR_INS', 'BR_MSP' are measured. @--counters INT_INS,FP_INS@ will+    *add* 'INT_INS' and 'FP_INS' to default set. @--counters+    =INT_INS,FP_INS@ will use only list of provided counters. Note+    that counter may or may not be supported on you CPU and there+    could be limit on number of counters used simultaneously.+-}+module Test.Tasty.PAPI+  ( -- * Running benchmarks+    Test.Tasty.PAPI.defaultMain+  , Benchmark+  , Benchmarkable(..)+  , bench+  , bgroup+    -- * Creation of Benchmarkable+  , nf+  , whnf+  , nfIO+  , whnfIO+    -- * Data types+  , Counter(..)+  ) where++import Control.Exception+import Control.Monad+import Control.DeepSeq+import Control.Concurrent.STM+import Data.List       (nub, intercalate)+import Data.Proxy+import Data.Foldable+import qualified Data.IntMap.Strict as IM+import qualified Data.Set           as Set+import Foreign.C+import Foreign.Ptr+import Foreign.Marshal+import Foreign.Storable+import System.Mem+import System.Exit+import System.IO+import Text.Printf++import Test.Tasty+import Test.Tasty.Ingredients+import Test.Tasty.Ingredients.ConsoleReporter+import Test.Tasty.Options+import Test.Tasty.Providers+import Test.Tasty.Runners+++----------------------------------------------------------------+-- Foreign calls to PAPI+----------------------------------------------------------------++-- PAPI eventset+newtype EventSet = EventSet CInt+  deriving (Show, Storable)++foreign import capi "papi.h value PAPI_OK"          papi_OK          :: CInt+foreign import capi "papi.h value PAPI_NULL"        papi_NULL        :: CInt+foreign import capi "papi.h value PAPI_VER_CURRENT" papi_VER_CURRENT :: CInt+ +foreign import capi "papi.h value PAPI_L1_DCM"   papi_L1_DCM  :: CInt+foreign import capi "papi.h value PAPI_L1_ICM"   papi_L1_ICM  :: CInt+foreign import capi "papi.h value PAPI_L2_DCM"   papi_L2_DCM  :: CInt+foreign import capi "papi.h value PAPI_L2_ICM"   papi_L2_ICM  :: CInt+foreign import capi "papi.h value PAPI_L3_DCM"   papi_L3_DCM  :: CInt+foreign import capi "papi.h value PAPI_L3_ICM"   papi_L3_ICM  :: CInt+foreign import capi "papi.h value PAPI_L1_TCM"   papi_L1_TCM  :: CInt+foreign import capi "papi.h value PAPI_L2_TCM"   papi_L2_TCM  :: CInt+foreign import capi "papi.h value PAPI_L3_TCM"   papi_L3_TCM  :: CInt+foreign import capi "papi.h value PAPI_CA_SNP"   papi_CA_SNP  :: CInt+foreign import capi "papi.h value PAPI_CA_SHR"   papi_CA_SHR  :: CInt+foreign import capi "papi.h value PAPI_CA_CLN"   papi_CA_CLN  :: CInt+foreign import capi "papi.h value PAPI_CA_INV"   papi_CA_INV  :: CInt+foreign import capi "papi.h value PAPI_CA_ITV"   papi_CA_ITV  :: CInt+foreign import capi "papi.h value PAPI_L3_LDM"   papi_L3_LDM  :: CInt+foreign import capi "papi.h value PAPI_L3_STM"   papi_L3_STM  :: CInt+foreign import capi "papi.h value PAPI_BRU_IDL"  papi_BRU_IDL :: CInt+foreign import capi "papi.h value PAPI_FXU_IDL"  papi_FXU_IDL :: CInt+foreign import capi "papi.h value PAPI_FPU_IDL"  papi_FPU_IDL :: CInt+foreign import capi "papi.h value PAPI_LSU_IDL"  papi_LSU_IDL :: CInt+foreign import capi "papi.h value PAPI_TLB_DM"   papi_TLB_DM  :: CInt+foreign import capi "papi.h value PAPI_TLB_IM"   papi_TLB_IM  :: CInt+foreign import capi "papi.h value PAPI_TLB_TL"   papi_TLB_TL  :: CInt+foreign import capi "papi.h value PAPI_L1_LDM"   papi_L1_LDM  :: CInt+foreign import capi "papi.h value PAPI_L1_STM"   papi_L1_STM  :: CInt+foreign import capi "papi.h value PAPI_L2_LDM"   papi_L2_LDM  :: CInt+foreign import capi "papi.h value PAPI_L2_STM"   papi_L2_STM  :: CInt+foreign import capi "papi.h value PAPI_BTAC_M"   papi_BTAC_M  :: CInt+foreign import capi "papi.h value PAPI_PRF_DM"   papi_PRF_DM  :: CInt+foreign import capi "papi.h value PAPI_L3_DCH"   papi_L3_DCH  :: CInt+foreign import capi "papi.h value PAPI_TLB_SD"   papi_TLB_SD  :: CInt+foreign import capi "papi.h value PAPI_CSR_FAL"  papi_CSR_FAL :: CInt+foreign import capi "papi.h value PAPI_CSR_SUC"  papi_CSR_SUC :: CInt+foreign import capi "papi.h value PAPI_CSR_TOT"  papi_CSR_TOT :: CInt+foreign import capi "papi.h value PAPI_MEM_SCY"  papi_MEM_SCY :: CInt+foreign import capi "papi.h value PAPI_MEM_RCY"  papi_MEM_RCY :: CInt+foreign import capi "papi.h value PAPI_MEM_WCY"  papi_MEM_WCY :: CInt+foreign import capi "papi.h value PAPI_STL_ICY"  papi_STL_ICY :: CInt+foreign import capi "papi.h value PAPI_FUL_ICY"  papi_FUL_ICY :: CInt+foreign import capi "papi.h value PAPI_STL_CCY"  papi_STL_CCY :: CInt+foreign import capi "papi.h value PAPI_FUL_CCY"  papi_FUL_CCY :: CInt+foreign import capi "papi.h value PAPI_HW_INT"   papi_HW_INT  :: CInt+foreign import capi "papi.h value PAPI_BR_UCN"   papi_BR_UCN  :: CInt+foreign import capi "papi.h value PAPI_BR_CN"    papi_BR_CN   :: CInt+foreign import capi "papi.h value PAPI_BR_TKN"   papi_BR_TKN  :: CInt+foreign import capi "papi.h value PAPI_BR_NTK"   papi_BR_NTK  :: CInt+foreign import capi "papi.h value PAPI_BR_MSP"   papi_BR_MSP  :: CInt+foreign import capi "papi.h value PAPI_BR_PRC"   papi_BR_PRC  :: CInt+foreign import capi "papi.h value PAPI_FMA_INS"  papi_FMA_INS :: CInt+foreign import capi "papi.h value PAPI_TOT_IIS"  papi_TOT_IIS :: CInt+foreign import capi "papi.h value PAPI_TOT_INS"  papi_TOT_INS :: CInt+foreign import capi "papi.h value PAPI_INT_INS"  papi_INT_INS :: CInt+foreign import capi "papi.h value PAPI_FP_INS"   papi_FP_INS  :: CInt+foreign import capi "papi.h value PAPI_LD_INS"   papi_LD_INS  :: CInt+foreign import capi "papi.h value PAPI_SR_INS"   papi_SR_INS  :: CInt+foreign import capi "papi.h value PAPI_BR_INS"   papi_BR_INS  :: CInt+foreign import capi "papi.h value PAPI_VEC_INS"  papi_VEC_INS :: CInt+foreign import capi "papi.h value PAPI_RES_STL"  papi_RES_STL :: CInt+foreign import capi "papi.h value PAPI_FP_STAL"  papi_FP_STAL :: CInt+foreign import capi "papi.h value PAPI_TOT_CYC"  papi_TOT_CYC :: CInt+foreign import capi "papi.h value PAPI_LST_INS"  papi_LST_INS :: CInt+foreign import capi "papi.h value PAPI_SYC_INS"  papi_SYC_INS :: CInt+foreign import capi "papi.h value PAPI_L1_DCH"   papi_L1_DCH  :: CInt+foreign import capi "papi.h value PAPI_L2_DCH"   papi_L2_DCH  :: CInt+foreign import capi "papi.h value PAPI_L1_DCA"   papi_L1_DCA  :: CInt+foreign import capi "papi.h value PAPI_L2_DCA"   papi_L2_DCA  :: CInt+foreign import capi "papi.h value PAPI_L3_DCA"   papi_L3_DCA  :: CInt+foreign import capi "papi.h value PAPI_L1_DCR"   papi_L1_DCR  :: CInt+foreign import capi "papi.h value PAPI_L2_DCR"   papi_L2_DCR  :: CInt+foreign import capi "papi.h value PAPI_L3_DCR"   papi_L3_DCR  :: CInt+foreign import capi "papi.h value PAPI_L1_DCW"   papi_L1_DCW  :: CInt+foreign import capi "papi.h value PAPI_L2_DCW"   papi_L2_DCW  :: CInt+foreign import capi "papi.h value PAPI_L3_DCW"   papi_L3_DCW  :: CInt+foreign import capi "papi.h value PAPI_L1_ICH"   papi_L1_ICH  :: CInt+foreign import capi "papi.h value PAPI_L2_ICH"   papi_L2_ICH  :: CInt+foreign import capi "papi.h value PAPI_L3_ICH"   papi_L3_ICH  :: CInt+foreign import capi "papi.h value PAPI_L1_ICA"   papi_L1_ICA  :: CInt+foreign import capi "papi.h value PAPI_L2_ICA"   papi_L2_ICA  :: CInt+foreign import capi "papi.h value PAPI_L3_ICA"   papi_L3_ICA  :: CInt+foreign import capi "papi.h value PAPI_L1_ICR"   papi_L1_ICR  :: CInt+foreign import capi "papi.h value PAPI_L2_ICR"   papi_L2_ICR  :: CInt+foreign import capi "papi.h value PAPI_L3_ICR"   papi_L3_ICR  :: CInt+foreign import capi "papi.h value PAPI_L1_ICW"   papi_L1_ICW  :: CInt+foreign import capi "papi.h value PAPI_L2_ICW"   papi_L2_ICW  :: CInt+foreign import capi "papi.h value PAPI_L3_ICW"   papi_L3_ICW  :: CInt+foreign import capi "papi.h value PAPI_L1_TCH"   papi_L1_TCH  :: CInt+foreign import capi "papi.h value PAPI_L2_TCH"   papi_L2_TCH  :: CInt+foreign import capi "papi.h value PAPI_L3_TCH"   papi_L3_TCH  :: CInt+foreign import capi "papi.h value PAPI_L1_TCA"   papi_L1_TCA  :: CInt+foreign import capi "papi.h value PAPI_L2_TCA"   papi_L2_TCA  :: CInt+foreign import capi "papi.h value PAPI_L3_TCA"   papi_L3_TCA  :: CInt+foreign import capi "papi.h value PAPI_L1_TCR"   papi_L1_TCR  :: CInt+foreign import capi "papi.h value PAPI_L2_TCR"   papi_L2_TCR  :: CInt+foreign import capi "papi.h value PAPI_L3_TCR"   papi_L3_TCR  :: CInt+foreign import capi "papi.h value PAPI_L1_TCW"   papi_L1_TCW  :: CInt+foreign import capi "papi.h value PAPI_L2_TCW"   papi_L2_TCW  :: CInt+foreign import capi "papi.h value PAPI_L3_TCW"   papi_L3_TCW  :: CInt+foreign import capi "papi.h value PAPI_FML_INS"  papi_FML_INS :: CInt+foreign import capi "papi.h value PAPI_FAD_INS"  papi_FAD_INS :: CInt+foreign import capi "papi.h value PAPI_FDV_INS"  papi_FDV_INS :: CInt+foreign import capi "papi.h value PAPI_FSQ_INS"  papi_FSQ_INS :: CInt+foreign import capi "papi.h value PAPI_FNV_INS"  papi_FNV_INS :: CInt++foreign import capi "papi.h PAPI_library_init"+  papi_library_init :: CInt -> IO CInt++foreign import capi unsafe "papi.h PAPI_create_eventset"+  papi_create_eventset :: Ptr EventSet -> IO CInt+foreign import capi unsafe "papi.h PAPI_cleanup_eventset"+  papi_cleanup_eventset :: EventSet -> IO CInt+foreign import capi unsafe "papi.h PAPI_destroy_eventset"+  papi_destroy_eventset :: Ptr EventSet -> IO CInt++foreign import capi unsafe "papi.h PAPI_add_event"+  papi_add_event :: EventSet -> CInt -> IO CInt++foreign import capi unsafe "papi.h PAPI_start"+  papi_start :: EventSet -> IO CInt+foreign import capi unsafe "papi.h PAPI_stop"+  papi_stop :: EventSet -> Ptr CLLong -> IO CInt+-- foreign import capi unsafe "papi.h PAPI_read"+--   papi_read :: CInt -> Ptr CLLong -> IO CInt+-- foreign import capi unsafe "papi.h PAPI_reset"+--   papi_reset :: CInt -> IO CInt++-- Call PAPI function and return error code+call :: IO CInt -> IO ()+call f = f >>= \case+  n | n == papi_OK -> pure ()+    | otherwise    -> error $ "PAPI call failed: " ++ show n++-- Create event set for use with PAPI+withPapiEventSet :: (EventSet -> IO a) -> IO a+withPapiEventSet action = do+  -- Initialize library. It seems that calling it multiple times is safe+  do n <- papi_library_init papi_VER_CURRENT+     when (n /= papi_VER_CURRENT) $ error "PAPI init failed"+  bracket ini fini action+  where+    ini = alloca $ \p_evt -> do+      poke p_evt (EventSet papi_NULL)+      call $ papi_create_eventset p_evt+      peek p_evt+    fini evt = do+      call $ papi_cleanup_eventset evt+      alloca $ \p_evt -> do+        poke p_evt evt+        call $ papi_destroy_eventset p_evt++-- | Supported hardware counters+-- +-- Documentation is taken from rather outdated manual:+-- https://icl.utk.edu/projects/papi/files/documentation/PAPI_USER_GUIDE_23.htm+data Counter+  = L1_DCM  -- ^ Level 1 data cache misses+  | L1_ICM  -- ^ Level 1 instruction cache misses+  | L2_DCM  -- ^ Level 2 data cache misses+  | L2_ICM  -- ^ Level 2 instruction cache misses+  | L3_DCM  -- ^ Level 3 data cache misses+  | L3_ICM  -- ^ Level 3 instruction cache misses+  | L1_TCM  -- ^ Level 1 total cache misses+  | L2_TCM  -- ^ Level 2 total cache misses+  | L3_TCM  -- ^ Level 3 total cache misses+  | CA_SNP  -- ^ Requests for a Snoop+  | CA_SHR  -- ^ Requests for access to shared cache line (SMP)+  | CA_CLN  -- ^ Requests for access to clean cache line (SMP)+  | CA_INV  -- ^ Cache Line Invalidation (SMP)+  | CA_ITV  -- ^ Cache Line Intervention (SMP)+  | L3_LDM  -- ^ Level 3 load misses+  | L3_STM  -- ^ Level 3 store misses+  | BRU_IDL -- ^ Cycles branch units are idle+  | FXU_IDL -- ^ Cycles integer units are idle+  | FPU_IDL -- ^ Cycles floating point units are idle+  | LSU_IDL -- ^ Cycles load/store units are idle+  | TLB_DM  -- ^ Data translation lookaside buffer misses+  | TLB_IM  -- ^ Instruction translation lookaside buffer misses+  | TLB_TL  -- ^ Total translation lookaside buffer misses+  | L1_LDM  -- ^ Level 1 load misses+  | L1_STM  -- ^ Level 1 store misses+  | L2_LDM  -- ^ Level 2 load misses+  | L2_STM  -- ^ Level 2 store misses+  | BTAC_M  -- ^ Branch target address cache (BTAC) misses+  | PRF_DM  -- ^ Pre-fetch data instruction caused a miss+  | L3_DCH  -- ^ Level 3 Data Cache Hit+  | TLB_SD  -- ^ Translation lookaside buffer shootdowns (SMP)+  | CSR_FAL -- ^ Failed store conditional instructions+  | CSR_SUC -- ^ Successful store conditional instructions+  | CSR_TOT -- ^ Total store conditional instructions+  | MEM_SCY -- ^ Cycles Stalled Waiting for Memory Access+  | MEM_RCY -- ^ Cycles Stalled Waiting for Memory Read+  | MEM_WCY -- ^ Cycles Stalled Waiting for Memory Write+  | STL_ICY -- ^ Cycles with No Instruction Issue+  | FUL_ICY -- ^ Cycles with Maximum Instruction Issue+  | STL_CCY -- ^ Cycles with No Instruction Completion+  | FUL_CCY -- ^ Cycles with Maximum Instruction Completion+  | HW_INT  -- ^ Hardware interrupts+  | BR_UCN  -- ^ Unconditional branch instructions executed+  | BR_CN   -- ^ Conditional branch instructions executed+  | BR_TKN  -- ^ Conditional branch instructions taken+  | BR_NTK  -- ^ Conditional branch instructions not taken+  | BR_MSP  -- ^ Conditional branch instructions mispredicted+  | BR_PRC  -- ^ Conditional branch instructions correctly predicted+  | FMA_INS -- ^ FMA instructions completed+  | TOT_IIS -- ^ Total instructions issued+  | TOT_INS -- ^ Total instructions executed+  | INT_INS -- ^ Integer instructions executed+  | FP_INS  -- ^ Floating point instructions executed+  | LD_INS  -- ^ Load instructions executed+  | SR_INS  -- ^ Store instructions executed+  | BR_INS  -- ^ Total branch instructions executed+  | VEC_INS -- ^ Vector/SIMD instructions executed+  | RES_STL -- ^ Cycles processor is stalled on resource+  | FP_STAL -- ^ Cycles any FP units are stalled+  | TOT_CYC -- ^ Total cycles+  | LST_INS -- ^ Total load/store instructions executed+  | SYC_INS -- ^ Synchronization instructions executed+  | L1_DCH  -- ^ L1 data cache hits+  | L2_DCH  -- ^ L2 data cache hits+  | L1_DCA  -- ^ L1 data cache accesses+  | L2_DCA  -- ^ L2 data cache accesses+  | L3_DCA  -- ^ L3 data cache accesses+  | L1_DCR  -- ^ L1 data cache reads+  | L2_DCR  -- ^ L2 data cache reads+  | L3_DCR  -- ^ L3 data cache reads+  | L1_DCW  -- ^ L1 data cache writes+  | L2_DCW  -- ^ L2 data cache writes+  | L3_DCW  -- ^ L3 data cache writes+  | L1_ICH  -- ^ L1 instruction cache hits+  | L2_ICH  -- ^ L2 instruction cache hits+  | L3_ICH  -- ^ L3 instruction cache hits+  | L1_ICA  -- ^ L1 instruction cache accesses+  | L2_ICA  -- ^ L2 instruction cache accesses+  | L3_ICA  -- ^ L3 instruction cache accesses+  | L1_ICR  -- ^ L1 instruction cache reads+  | L2_ICR  -- ^ L2 instruction cache reads+  | L3_ICR  -- ^ L3 instruction cache reads+  | L1_ICW  -- ^ L1 instruction cache writes+  | L2_ICW  -- ^ L2 instruction cache writes+  | L3_ICW  -- ^ L3 instruction cache writes+  | L1_TCH  -- ^ L1 total cache hits+  | L2_TCH  -- ^ L2 total cache hits+  | L3_TCH  -- ^ L3 total cache hits+  | L1_TCA  -- ^ L1 total cache accesses+  | L2_TCA  -- ^ L2 total cache accesses+  | L3_TCA  -- ^ L3 total cache accesses+  | L1_TCR  -- ^ L1 total cache reads+  | L2_TCR  -- ^ L2 total cache reads+  | L3_TCR  -- ^ L3 total cache reads+  | L1_TCW  -- ^ L1 total cache writes+  | L2_TCW  -- ^ L2 total cache writes+  | L3_TCW  -- ^ L3 total cache writes+  | FML_INS -- ^ Floating Multiply instructions+  | FAD_INS -- ^ Floating Add instructions+  | FDV_INS -- ^ Floating Divide instructions+  | FSQ_INS -- ^ Floating Square Root instructions+  | FNV_INS -- ^ Floating Inverse instructions+  deriving (Show,Read,Eq,Ord)+++toCounter :: Counter -> CInt+toCounter = \case+  L1_DCM  -> papi_L1_DCM+  L1_ICM  -> papi_L1_ICM+  L2_DCM  -> papi_L2_DCM+  L2_ICM  -> papi_L2_ICM+  L3_DCM  -> papi_L3_DCM+  L3_ICM  -> papi_L3_ICM+  L1_TCM  -> papi_L1_TCM+  L2_TCM  -> papi_L2_TCM+  L3_TCM  -> papi_L3_TCM+  CA_SNP  -> papi_CA_SNP+  CA_SHR  -> papi_CA_SHR+  CA_CLN  -> papi_CA_CLN+  CA_INV  -> papi_CA_INV+  CA_ITV  -> papi_CA_ITV+  L3_LDM  -> papi_L3_LDM+  L3_STM  -> papi_L3_STM+  BRU_IDL -> papi_BRU_IDL+  FXU_IDL -> papi_FXU_IDL+  FPU_IDL -> papi_FPU_IDL+  LSU_IDL -> papi_LSU_IDL+  TLB_DM  -> papi_TLB_DM+  TLB_IM  -> papi_TLB_IM+  TLB_TL  -> papi_TLB_TL+  L1_LDM  -> papi_L1_LDM+  L1_STM  -> papi_L1_STM+  L2_LDM  -> papi_L2_LDM+  L2_STM  -> papi_L2_STM+  BTAC_M  -> papi_BTAC_M+  PRF_DM  -> papi_PRF_DM+  L3_DCH  -> papi_L3_DCH+  TLB_SD  -> papi_TLB_SD+  CSR_FAL -> papi_CSR_FAL+  CSR_SUC -> papi_CSR_SUC+  CSR_TOT -> papi_CSR_TOT+  MEM_SCY -> papi_MEM_SCY+  MEM_RCY -> papi_MEM_RCY+  MEM_WCY -> papi_MEM_WCY+  STL_ICY -> papi_STL_ICY+  FUL_ICY -> papi_FUL_ICY+  STL_CCY -> papi_STL_CCY+  FUL_CCY -> papi_FUL_CCY+  HW_INT  -> papi_HW_INT+  BR_UCN  -> papi_BR_UCN+  BR_CN   -> papi_BR_CN+  BR_TKN  -> papi_BR_TKN+  BR_NTK  -> papi_BR_NTK+  BR_MSP  -> papi_BR_MSP+  BR_PRC  -> papi_BR_PRC+  FMA_INS -> papi_FMA_INS+  TOT_IIS -> papi_TOT_IIS+  TOT_INS -> papi_TOT_INS+  INT_INS -> papi_INT_INS+  FP_INS  -> papi_FP_INS+  LD_INS  -> papi_LD_INS+  SR_INS  -> papi_SR_INS+  BR_INS  -> papi_BR_INS+  VEC_INS -> papi_VEC_INS+  RES_STL -> papi_RES_STL+  FP_STAL -> papi_FP_STAL+  TOT_CYC -> papi_TOT_CYC+  LST_INS -> papi_LST_INS+  SYC_INS -> papi_SYC_INS+  L1_DCH  -> papi_L1_DCH+  L2_DCH  -> papi_L2_DCH+  L1_DCA  -> papi_L1_DCA+  L2_DCA  -> papi_L2_DCA+  L3_DCA  -> papi_L3_DCA+  L1_DCR  -> papi_L1_DCR+  L2_DCR  -> papi_L2_DCR+  L3_DCR  -> papi_L3_DCR+  L1_DCW  -> papi_L1_DCW+  L2_DCW  -> papi_L2_DCW+  L3_DCW  -> papi_L3_DCW+  L1_ICH  -> papi_L1_ICH+  L2_ICH  -> papi_L2_ICH+  L3_ICH  -> papi_L3_ICH+  L1_ICA  -> papi_L1_ICA+  L2_ICA  -> papi_L2_ICA+  L3_ICA  -> papi_L3_ICA+  L1_ICR  -> papi_L1_ICR+  L2_ICR  -> papi_L2_ICR+  L3_ICR  -> papi_L3_ICR+  L1_ICW  -> papi_L1_ICW+  L2_ICW  -> papi_L2_ICW+  L3_ICW  -> papi_L3_ICW+  L1_TCH  -> papi_L1_TCH+  L2_TCH  -> papi_L2_TCH+  L3_TCH  -> papi_L3_TCH+  L1_TCA  -> papi_L1_TCA+  L2_TCA  -> papi_L2_TCA+  L3_TCA  -> papi_L3_TCA+  L1_TCR  -> papi_L1_TCR+  L2_TCR  -> papi_L2_TCR+  L3_TCR  -> papi_L3_TCR+  L1_TCW  -> papi_L1_TCW+  L2_TCW  -> papi_L2_TCW+  L3_TCW  -> papi_L3_TCW+  FML_INS -> papi_FML_INS+  FAD_INS -> papi_FAD_INS+  FDV_INS -> papi_FDV_INS+  FSQ_INS -> papi_FSQ_INS+  FNV_INS -> papi_FNV_INS+++----------------------------------------------------------------+--+----------------------------------------------------------------++++newtype CsvPath = CsvPath FilePath++instance IsOption (Maybe CsvPath) where+  defaultValue = Nothing+  parseValue = Just . Just . CsvPath+  optionName = pure "csv"+  optionHelp = pure "File to write results in CSV format"++-- Set of counters to use+newtype CounterSet = CounterSet { getCounterSet :: [Counter] }++instance IsOption CounterSet where+  defaultValue = CounterSet [TOT_INS, TOT_CYC, BR_INS, BR_MSP]+  optionName = pure "counters"+  optionHelp = pure "Adjust set of counters to use"+  parseValue = \case+    ('=':s) -> CounterSet <$> parser s+    s       -> CounterSet . nub . (def++) <$> parser s+    where+      CounterSet def = defaultValue+      parser s = case reads s of+        [(c,"")]     -> Just [c]+        [(c,',':s')] -> (c:) <$> parser s'+        _            -> Nothing+++----------------------------------------------------------------+-- Running benchmarks+----------------------------------------------------------------++-- | Just a 'TestTree'. This type synonym is provided for source compatibility with+--   @criterion@ and @gauge@.+--+-- @since 0.1+type Benchmark = TestTree++-- | IO action which could be benchmarked. It's created by 'whnf',+--   'nf', 'whnfIO', 'nfIO'.+--+-- @since 0.1+newtype Benchmarkable = Benchmarkable (IO ())++instance IsTest Benchmarkable where+  testOptions = pure []+  run opts (Benchmarkable io) _+    | 1 == n_threads = do+        withPapiEventSet $ \evt -> do+          forM_ counters $ call . papi_add_event evt . toCounter+          allocaArray (length counters) $ \vals -> do+            -- Evaluate benchmark once in order to ensure that all+            -- parameters are evaluated. Consider benchmarks+            --+            -- > bench "A" $ nf funA xs+            -- > bench "B" $ nf funB xs+            --+            -- Without calling `io' we'll count instruction needed to+            -- evaluate xs as well!+            io+            -- We don't want to GC happen in the middle of benchmark+            -- just because previous benchmarks allocated enough to+            -- trigger it. This could bias measurement a lot since we+            -- run bencmark only once+            performMajorGC+            n1 <- getAllocationCounter+            -- Perform measurement+            call $ papi_start evt+            io+            call $ papi_stop evt vals+            n2 <- getAllocationCounter+            let n_alloc = fromIntegral $ n1-n2+            -- Read data+            measurements <- traverse (peekElemOff vals . fst) $ [0..] `zip` counters+            pure $ testPassed+                 $ show (n_alloc:measurements)+                ++ intercalate "\t"+                     ( ("ALLOC="++showN n_alloc)+                     : [ show c ++ ('=':showN n)+                       | (c,n) <- zip counters measurements])+++    | otherwise = pure $ testFailed+        "Benchmarks must not be run concurrently. Please pass -j1 or use single threaded runtime."+    where+      n_threads = getNumThreads $ lookupOption opts+      counters  = getCounterSet $ lookupOption opts++showN :: CLLong -> String+showN n+  | x < 1e4 = show n+  --+  | x < 1e5 = printf "%.2fe3" (x/1e3)+  | x < 1e6 = printf "%.1fe3" (x/1e3)+  --+  | x < 1e7 = printf "%.3fe6" (x/1e6)+  | x < 1e8 = printf "%.2fe6" (x/1e6)+  | x < 1e9 = printf "%.1fe6" (x/1e6)+  --+  | x < 1e10 = printf "%.3fe9" (x/1e9)+  | x < 1e11 = printf "%.2fe9" (x/1e9)+  | x < 1e12 = printf "%.1fe9" (x/1e9)+  | otherwise = printf "%.3e" x+  where x = fromIntegral n :: Double++-- | Create single benchmark. This is just a monomorphization of+--   'singleTest' which provides API compatibility with @criterion@+--   and @gauge@.+--+-- @since 0.1+bench :: String -> Benchmarkable -> TestTree+bench = singleTest++-- | Create single benchmark. This is just a 'testGroup' and it exists+--   to provide API compatibility with @criterion@ and @gauge@.+--+-- @since 0.1+bgroup :: String -> [Benchmark] -> Benchmark+bgroup = testGroup++-- | @nf f x@ measures number of instructions needed to compute normal+--   form of and application of @f@ to @x@.+--+-- @since 0.1+nf :: NFData b => (a -> b) -> a -> Benchmarkable+{-# NOINLINE nf #-}+nf f a = Benchmarkable $ do _ <- evaluate $ force (f a)+                            return ()++-- | @nf f x@ measures number of instructions needed to compute weak+--   head normal form of and application of @f@ to @x@.+--+-- @since 0.1+whnf :: (a -> b) -> a -> Benchmarkable+{-# NOINLINE whnf #-}+whnf f a = Benchmarkable $ do _ <- evaluate (f a)+                              return ()++-- | @whnfIO a@ measures number of instructions needed to evaluate IO+--   action and reduce value returned by it to weak head normal form.+--+-- @since 0.1+whnfIO :: IO a -> Benchmarkable+{-# NOINLINE whnfIO #-}+whnfIO io = Benchmarkable $ do _ <- evaluate =<< io+                               return ()+-- | @nfIO a@ measures number of instructions needed to evaluate IO+--   action and reduce value returned by it to normal form.+--+-- @since 0.1+nfIO :: NFData a => IO a -> Benchmarkable+{-# NOINLINE nfIO #-}+nfIO io = Benchmarkable $ do a <- io+                             _ <- evaluate (force a)+                             return ()++-- | Run benchmark suite. It provides API compatible with @criterion@ and @gauge@.+--+-- @since 0.1+defaultMain :: [TestTree] -> IO ()+defaultMain+  = defaultMainWithIngredients [ listingTests+                               , consoleBenchReporter `composeReporters` csvReporter+                               ]+  . testGroup "All"+++----------------------------------------------------------------+-- Reporters+----------------------------------------------------------------++consoleBenchReporter :: Ingredient+consoleBenchReporter = consoleTestReporterWithHook $ \_ r -> do+  case reads @[CLLong] $ resultDescription r of+    [(_,s)] -> pure r { resultDescription = s }+    _       -> pure r++csvReporter :: Ingredient+csvReporter = TestReporter [Option (Proxy @(Maybe CsvPath)), Option (Proxy @CounterSet)] $+  \opts tree -> do+    CsvPath path <- lookupOption opts+    let CounterSet counters = lookupOption opts+    let names    = testsNames opts tree+        namesMap = IM.fromDistinctAscList $ zip [0..] names+    pure $ \smap -> do+      case findNonUniqueElement names of+        Nothing -> pure ()+        Just name -> die $ "CSV report cannot proceed, because name '" ++ name+                        ++ "' corresponds to two or more benchmarks. Please disambiguate them."+      withFile path WriteMode $ \h -> do+        hSetBuffering h LineBuffering+        hPutStrLn h $ intercalate "," $ "ALLOC" : (show <$> counters)+        csvOutput h $ IM.intersectionWith (,) namesMap smap+      pure $ \_ -> isSuccessful smap++isSuccessful :: StatusMap -> IO Bool+isSuccessful = go . IM.elems+  where+    go [] = pure True+    go (tv : tvs) = do+      b <- atomically $ readTVar tv >>= \s -> case s of Done r -> pure (resultSuccessful r); _ -> retry+      if b then go tvs else pure False++findNonUniqueElement :: Ord a => [a] -> Maybe a+findNonUniqueElement = go mempty+  where+    go _   [] = Nothing+    go acc (x : xs)+      | x `Set.member` acc = Just x+      | otherwise          = go (Set.insert x acc) xs++csvOutput :: Handle -> IM.IntMap (TestName, TVar Status) -> IO ()+csvOutput h = traverse_ $ \(name, tv) -> do+  r <- atomically $ readTVar tv >>= \case+    Done r -> pure r+    _      -> retry+  case reads @[CLLong] $ resultDescription r of+    [(meas,_)] -> hPutStrLn h $ intercalate "," $ encodeCsv name : (show <$> meas)+    _          -> pure ()++encodeCsv :: String -> String+encodeCsv xs+  | any (`elem` xs) ",\"\n\r" = '"' : go xs+  | otherwise                 = xs+  where+    go []         = '"' : []+    go ('"' : ys) = '"' : '"' : go ys+    go (y   : ys) = y   : go ys
+ tasty-papi.cabal view
@@ -0,0 +1,51 @@+name:          tasty-papi+version:       0.1+cabal-version: 1.18+license:       BSD3+license-file:  LICENSE+copyright:     2023, Alexey Khudyakov+author:        Alexey Khudyakov <alexey.skladnoy@gmail.com>+maintainer:    Alexey Khudyakov <alexey.skladnoy@gmail.com>+homepage:      https://github.com/Shimuuar/tasty-bench+bug-reports:   https://github.com/Bodigrim/tasty-bench/issues+category:      Development, Performance, Testing, Benchmarking+build-type:    Simple++Synopsis:+  Bencmarking using instruction counting+description:+  Benchmarking framework which uses instruction counters intead of+  time measurements. Instruction count is only proxy for code+  performance but unlike time could be measured deterministically.+  PAPI is used in order to access hardware counters. ++extra-source-files:+  README.md+  Changelog.md++source-repository head+  type:     git+  location: http://github.com/Shimuuar/tasty-papi++Library+  default-language: Haskell2010+  ghc-options:      -Wall+  --+  Build-Depends:    base            >=4.10 && <5+                  , tasty           >=0.11.3+                  , containers      >=0.5+                  , deepseq+                  , stm+  extra-libraries: papi+  Exposed-modules: Test.Tasty.PAPI++test-suite tasty-papi-run+  default-language: Haskell2010+  type:             exitcode-stdio-1.0+  ghc-options:      -Wall+  main-is:          Main.hs+  hs-source-dirs:   tests+  --+  Build-Depends:    base        -any+                  , tasty       -any+                  , tasty-papi  -any
+ tests/Main.hs view
@@ -0,0 +1,38 @@+-- |+module Main where++import Control.Monad+import Data.IORef+import Data.Function+import Test.Tasty.PAPI++main :: IO ()+main = defaultMain+  [ bgroup "Fib"+    [ bench (show n) $ whnf fib n+    | n <- [6..10]+    ]+  , bgroup "Parameter evaluation"+    [ bench "Eval1" $ whnf sum constant+    , bench "Eval2" $ whnf sum constant+    ]+  , bgroup "Misc"+    [ -- Show performance regression between 9.2 and 9.4+      bench "IORef regression" $ whnfIO $ do+        ref <- newIORef (1000 :: Int)+        fix $ \loop -> do+          x <- readIORef ref+          unless (x == 0) $ do+            writeIORef ref $ x - 1+            loop+        readIORef ref+    ]+  ]++fib :: Integer -> Integer+fib 0 = 0+fib 1 = 1+fib n = fib (n-1) + fib (n-2)++constant :: [Int]+constant = [1 .. 1000]