Skip to content

Commit d7d4405

Browse files
committed
Add test for allocations
This is good way to check that GHC generate tight non-allocating loops for monadic/applicative functions.
1 parent c92915b commit d7d4405

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE ScopedTypeVariables #-}
3+
{-# LANGUAGE TypeApplications #-}
4+
{- |
5+
Here we test that GHC is able to optimize well construction of vector
6+
using monadic\/applicative actions. Well is understood as able to
7+
generate code which does not allocate except for buffer and some
8+
constant overhead.
9+
-}
10+
module Inspect.Alloc where
11+
12+
import Data.Int
13+
-- import Data.Monoid
14+
import Data.Functor.Identity
15+
import Test.Tasty
16+
import Test.Tasty.HUnit
17+
import System.Mem
18+
import Test.Alloc
19+
20+
import qualified Data.Vector.Unboxed as VU
21+
22+
23+
tests :: TestTree
24+
tests = testGroup "allocations"
25+
[ testGroup "traversable"
26+
[ testCase "IO"
27+
$ checkAllocations (linear 8)
28+
$ whnfIO (VU.traverse (\_ -> getAllocationCounter) vector)
29+
, testCase "Identity"
30+
$ checkAllocations (linear 8)
31+
$ VU.traverse (\n -> Identity (10*n)) `whnf` vector
32+
-- NOTE: Naive traversal is lazy and allocated 2 words per element
33+
--
34+
-- , testCase "Const Sum"
35+
-- $ checkAllocations constant
36+
-- $ whnf (VU.traverse (Const @_ @() . Sum)) vector
37+
]
38+
, testGroup "unstreamM"
39+
[ testCase "IO"
40+
$ checkAllocations (linear 8)
41+
$ whnfIO (VU.replicateM size getAllocationCounter)
42+
-- , testCase "Identity"
43+
-- $ checkAllocations (linear 8)
44+
-- $ (\sz -> VU.generateM sz (\n -> Identity (fromIntegral n :: Int64))) `whnf` size
45+
]
46+
]
47+
48+
49+
-- | Constant overhead. Measurement precision is 4k
50+
overhead :: Int64
51+
overhead = 4096*2
52+
53+
-- | Vector size. It should be large so 1byte per element will be
54+
-- large than page.
55+
size :: Int
56+
size = 100000
57+
58+
vector :: VU.Vector Int64
59+
{-# NOINLINE vector #-}
60+
vector = VU.generate size fromIntegral
61+
62+
-- | N bytes per element + constant overhead. We also check that bound
63+
-- is tight.
64+
linear :: Int -> Range
65+
linear n = Range
66+
{ allocHi = fromIntegral (n * size) + overhead
67+
, allocLo = fromIntegral (n * size)
68+
}
69+
70+
-- | Only constant overhead
71+
constant :: Range
72+
constant = Range { allocHi = overhead
73+
, allocLo = 0
74+
}

vector/tests-inspect/Test/Alloc.hs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
-- |
3+
-- Test that function allocates is in range. This is good way to test
4+
-- that GHC produces tight non-allocating loops.
5+
module Test.Alloc where
6+
7+
import Control.Exception
8+
import Data.Int
9+
import System.Mem
10+
import Test.Tasty.HUnit
11+
import Text.Printf
12+
13+
----------------------------------------------------------------
14+
-- Benchmarking machinery copied from tasty-bench
15+
----------------------------------------------------------------
16+
17+
newtype Benchmarkable = Benchmarkable (IO ())
18+
19+
whnf :: (a -> b) -> a -> Benchmarkable
20+
{-# NOINLINE whnf #-}
21+
whnf f a = Benchmarkable $ do _ <- evaluate (f a)
22+
return ()
23+
24+
whnfIO :: IO a -> Benchmarkable
25+
{-# NOINLINE whnfIO #-}
26+
whnfIO io = Benchmarkable $ do _ <- evaluate =<< io
27+
return ()
28+
29+
30+
-- | Measure allocations. Measurements use 'getAllocationCounter' so
31+
-- it's accurate up to 4k bytes.
32+
allocations :: Benchmarkable -> IO Int64
33+
allocations (Benchmarkable io) = do
34+
-- We need to run `io` twice in order to ensure that all constant
35+
-- parameters are evaluated.
36+
io
37+
n1 <- getAllocationCounter
38+
io
39+
n2 <- getAllocationCounter
40+
return $! n1 - n2
41+
42+
43+
-- | Expected allocations range
44+
data Range = Range { allocLo :: !Int64
45+
, allocHi :: !Int64
46+
}
47+
deriving Show
48+
49+
-- | Check that computation's allocations lie in range
50+
checkAllocations :: Range -> Benchmarkable -> IO ()
51+
checkAllocations Range{..} bench = do
52+
alloc <- allocations bench
53+
let msg = unlines [ printf "allocated = %12d" alloc
54+
, printf "Low bound = %12d" allocLo
55+
, printf "Hi bound = %12d" allocHi
56+
]
57+
assertBool msg $ alloc <= allocHi
58+
&& alloc >= allocLo
59+

vector/tests-inspect/main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
module Main (main) where
33

44
import qualified Inspect
5+
import qualified Inspect.Alloc
56
#if MIN_VERSION_base(4,12,0)
67
import qualified Inspect.DerivingVia
78
#endif
@@ -13,4 +14,5 @@ main = defaultMain $ testGroup "tests"
1314
#if MIN_VERSION_base(4,12,0)
1415
, Inspect.DerivingVia.tests
1516
#endif
17+
, Inspect.Alloc.tests
1618
]

vector/vector.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ test-suite vector-inspection
261261
main-is: main.hs
262262
default-language: Haskell2010
263263
Other-modules: Inspect
264+
Inspect.Alloc
265+
Test.Alloc
264266
if impl(ghc >= 8.6)
265267
Other-modules: Inspect.DerivingVia
266268
Inspect.DerivingVia.OtherFoo
@@ -269,6 +271,7 @@ test-suite vector-inspection
269271
, primitive >= 0.6.4.0 && < 0.10
270272
, vector -any
271273
, tasty
274+
, tasty-hunit
272275
, tasty-inspection-testing >= 0.1
273276

274277
library benchmarks-O2

0 commit comments

Comments
 (0)