Skip to content

Commit c651094

Browse files
authored
Optimize IntMap.Bin (#995)
* IntMap: Merge prefix and mask in Bin * Update lookupGE benchmarks * Add note on the relation to the Okasaki-Gill paper * Adopt BurningWitness's lower/upper helpers * Add property tests for is{Proper}SubmapOfBy * Generate large keys in Arbitrary IntMap for better coverage * Add GHC flag to prevent benchmarks from changing due to unpredictable alignment changes
1 parent 855d6f8 commit c651094

File tree

6 files changed

+758
-551
lines changed

6 files changed

+758
-551
lines changed

containers-tests/benchmarks/LookupGE/LookupGE_IntMap.hs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ lookupGE1 k m =
1212

1313
lookupGE2 :: Key -> IntMap a -> Maybe (Key,a)
1414
lookupGE2 k t = case t of
15-
Bin _ m l r | m < 0 -> if k >= 0
15+
Bin p l r | signBranch p -> if k >= 0
1616
then go l
1717
else case go r of
1818
Nothing -> Just $ findMin l
1919
justx -> justx
2020
_ -> go t
2121
where
22-
go (Bin p m l r)
23-
| nomatch k p m = if k < p
22+
go (Bin p l r)
23+
| nomatch k p = if k < unPrefix p
2424
then Just $ findMin l
2525
else Nothing
26-
| zero k m = case go l of
26+
| left k p = case go l of
2727
Nothing -> Just $ findMin r
2828
justx -> justx
2929
| otherwise = go r
@@ -34,14 +34,14 @@ lookupGE2 k t = case t of
3434

3535
lookupGE3 :: Key -> IntMap a -> Maybe (Key,a)
3636
lookupGE3 k t = k `seq` case t of
37-
Bin _ m l r | m < 0 -> if k >= 0
37+
Bin p l r | signBranch p -> if k >= 0
3838
then go Nothing l
3939
else go (Just (findMin l)) r
4040
_ -> go Nothing t
4141
where
42-
go def (Bin p m l r)
43-
| nomatch k p m = if k < p then Just $ findMin l else def
44-
| zero k m = go (Just $ findMin r) l
42+
go def (Bin p l r)
43+
| nomatch k p = if k < unPrefix p then Just $ findMin l else def
44+
| left k p = go (Just $ findMin r) l
4545
| otherwise = go def r
4646
go def (Tip ky y)
4747
| k > ky = def
@@ -50,13 +50,13 @@ lookupGE3 k t = k `seq` case t of
5050

5151
lookupGE4 :: Key -> IntMap a -> Maybe (Key,a)
5252
lookupGE4 k t = k `seq` case t of
53-
Bin _ m l r | m < 0 -> if k >= 0 then go Nil l
54-
else go l r
53+
Bin p l r | signBranch p -> if k >= 0 then go Nil l
54+
else go l r
5555
_ -> go Nil t
5656
where
57-
go def (Bin p m l r)
58-
| nomatch k p m = if k < p then fMin l else fMin def
59-
| zero k m = go r l
57+
go def (Bin p l r)
58+
| nomatch k p = if k < unPrefix p then fMin l else fMin def
59+
| left k p = go r l
6060
| otherwise = go def r
6161
go def (Tip ky y)
6262
| k > ky = fMin def
@@ -66,7 +66,7 @@ lookupGE4 k t = k `seq` case t of
6666
fMin :: IntMap a -> Maybe (Key, a)
6767
fMin Nil = Nothing
6868
fMin (Tip ky y) = Just (ky, y)
69-
fMin (Bin _ _ l _) = fMin l
69+
fMin (Bin _ l _) = fMin l
7070

7171
-------------------------------------------------------------------------------
7272
-- Utilities

containers-tests/containers-tests.cabal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ common benchmark-deps
5757
, deepseq >=1.1.0.0 && <1.6
5858
, tasty-bench >=0.3.1 && <0.4
5959

60+
-- Flags recommended by tasty-bench
61+
if impl(ghc >= 8.6)
62+
ghc-options: -fproc-alignment=64
63+
6064
-- Copy of containers library,
6165
library
6266
import: deps
@@ -70,7 +74,11 @@ library
7074

7175
include-dirs: include
7276
hs-source-dirs: src, tests
77+
7378
ghc-options: -O2 -Wall
79+
if impl(ghc >= 8.6)
80+
ghc-options: -fproc-alignment=64
81+
7482
other-extensions:
7583
BangPatterns
7684
CPP
Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
module IntMapValidity (valid) where
1+
module IntMapValidity
2+
( valid
3+
, hasPrefix
4+
, hasPrefixSimple
5+
) where
26

3-
import Data.Bits (xor, (.&.))
7+
import Data.Bits (finiteBitSize, testBit, xor, (.&.))
8+
import Data.List (intercalate, elemIndex)
49
import Data.IntMap.Internal
10+
import Numeric (showHex)
511
import Test.Tasty.QuickCheck (Property, counterexample, property, (.&&.))
6-
import Utils.Containers.Internal.BitUtil (bitcount)
712

813
{--------------------------------------------------------------------
914
Assertions
@@ -12,54 +17,61 @@ import Utils.Containers.Internal.BitUtil (bitcount)
1217
valid :: IntMap a -> Property
1318
valid t =
1419
counterexample "nilNeverChildOfBin" (nilNeverChildOfBin t) .&&.
15-
counterexample "commonPrefix" (commonPrefix t) .&&.
16-
counterexample "maskRespected" (maskRespected t)
20+
counterexample "prefixOk" (prefixOk t)
1721

1822
-- Invariant: Nil is never found as a child of Bin.
1923
nilNeverChildOfBin :: IntMap a -> Bool
2024
nilNeverChildOfBin t =
2125
case t of
2226
Nil -> True
2327
Tip _ _ -> True
24-
Bin _ _ l r -> noNilInSet l && noNilInSet r
28+
Bin _ l r -> noNilInSet l && noNilInSet r
2529
where
2630
noNilInSet t' =
2731
case t' of
2832
Nil -> False
2933
Tip _ _ -> True
30-
Bin _ _ l' r' -> noNilInSet l' && noNilInSet r'
34+
Bin _ l' r' -> noNilInSet l' && noNilInSet r'
3135

32-
-- Invariant: The Mask is a power of 2. It is the largest bit position at which
33-
-- two keys of the map differ.
34-
maskPowerOfTwo :: IntMap a -> Bool
35-
maskPowerOfTwo t =
36+
-- Invariants:
37+
-- * All keys in a Bin start with the Bin's shared prefix.
38+
-- * All keys in the Bin's left child have the Prefix's mask bit unset.
39+
-- * All keys in the Bin's right child have the Prefix's mask bit set.
40+
prefixOk :: IntMap a -> Property
41+
prefixOk t =
3642
case t of
37-
Nil -> True
38-
Tip _ _ -> True
39-
Bin _ m l r ->
40-
bitcount 0 (fromIntegral m) == 1 && maskPowerOfTwo l && maskPowerOfTwo r
43+
Nil -> property ()
44+
Tip _ _ -> property ()
45+
Bin p l r ->
46+
let px = unPrefix p
47+
m = px .&. (-px)
48+
keysl = keys l
49+
keysr = keys r
50+
debugStr = concat
51+
[ "px=" ++ showIntHex px
52+
, ", keysl=[" ++ intercalate "," (fmap showIntHex keysl) ++ "]"
53+
, ", keysr=[" ++ intercalate "," (fmap showIntHex keysr) ++ "]"
54+
]
55+
in counterexample debugStr $
56+
counterexample "mask bit absent" (px /= 0) .&&.
57+
counterexample "prefix not shared" (all (`hasPrefix` p) (keysl ++ keysr)) .&&.
58+
counterexample "left child, mask found set" (all (\x -> x .&. m == 0) keysl) .&&.
59+
counterexample "right child, mask found unset" (all (\x -> x .&. m /= 0) keysr)
4160

42-
-- Invariant: Prefix is the common high-order bits that all elements share to
43-
-- the left of the Mask bit.
44-
commonPrefix :: IntMap a -> Bool
45-
commonPrefix t =
46-
case t of
47-
Nil -> True
48-
Tip _ _ -> True
49-
b@(Bin p _ l r) -> all (sharedPrefix p) (keys b) && commonPrefix l && commonPrefix r
61+
hasPrefix :: Int -> Prefix -> Bool
62+
hasPrefix i p = not (nomatch i p)
63+
64+
-- We test that hasPrefix behaves the same as hasPrefixSimple.
65+
hasPrefixSimple :: Int -> Prefix -> Bool
66+
hasPrefixSimple k p = case elemIndex True pbits of
67+
Nothing -> error "no mask bit" -- should not happen
68+
Just i -> drop (i+1) kbits == drop (i+1) pbits
5069
where
51-
sharedPrefix :: Prefix -> Int -> Bool
52-
sharedPrefix p a = p == p .&. a
70+
kbits = toBits k
71+
pbits = toBits (unPrefix p)
5372

54-
-- Invariant: In Bin prefix mask left right, left consists of the elements that
55-
-- don't have the mask bit set; right is all the elements that do.
56-
maskRespected :: IntMap a -> Bool
57-
maskRespected t =
58-
case t of
59-
Nil -> True
60-
Tip _ _ -> True
61-
Bin _ binMask l r ->
62-
all (\x -> zero x binMask) (keys l) &&
63-
all (\x -> not (zero x binMask)) (keys r) &&
64-
maskRespected l &&
65-
maskRespected r
73+
-- Bits from lowest to highest.
74+
toBits x = fmap (testBit x) [0 .. finiteBitSize (0 :: Int) - 1]
75+
76+
showIntHex :: Int -> String
77+
showIntHex x = "0x" ++ showHex (fromIntegral x :: Word) ""

containers-tests/tests/intmap-properties.hs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import Data.IntMap.Internal (traverseMaybeWithKey)
1010
import Data.IntMap.Merge.Lazy
1111
#endif
1212
import Data.IntMap.Internal.Debug (showTree)
13-
import IntMapValidity (valid)
13+
import Data.IntMap.Internal (Prefix(..))
14+
import IntMapValidity (hasPrefix, hasPrefixSimple, valid)
1415

1516
import Control.Applicative (Applicative(..))
1617
import Control.Monad ((<=<))
@@ -134,6 +135,7 @@ main = defaultMain $ testGroup "intmap-properties"
134135
, testCase "minimum" test_minimum
135136
, testCase "maximum" test_maximum
136137
, testProperty "valid" prop_valid
138+
, testProperty "hasPrefix" prop_hasPrefix
137139
, testProperty "empty valid" prop_emptyValid
138140
, testProperty "insert to singleton" prop_singleton
139141
, testProperty "insert then lookup" prop_insertLookup
@@ -209,6 +211,8 @@ main = defaultMain $ testGroup "intmap-properties"
209211
, testProperty "traverseMaybeWithKey identity" prop_traverseMaybeWithKey_identity
210212
, testProperty "traverseMaybeWithKey->mapMaybeWithKey" prop_traverseMaybeWithKey_degrade_to_mapMaybeWithKey
211213
, testProperty "traverseMaybeWithKey->traverseWithKey" prop_traverseMaybeWithKey_degrade_to_traverseWithKey
214+
, testProperty "isProperSubmapOfBy" prop_isProperSubmapOfBy
215+
, testProperty "isSubmapOfBy" prop_isSubmapOfBy
212216
]
213217

214218
apply2 :: Fun (a, b) c -> a -> b -> c
@@ -223,12 +227,23 @@ apply3 f a b c = apply f (a, b, c)
223227
--------------------------------------------------------------------}
224228

225229
instance Arbitrary a => Arbitrary (IntMap a) where
226-
arbitrary = fmap fromList arbitrary
230+
arbitrary = oneof [go arbitrary, go (getLarge <$> arbitrary)]
231+
where
232+
go kgen = fromList <$> listOf ((,) <$> kgen <*> arbitrary)
233+
shrink = fmap fromList . shrink . toAscList
227234

228235
newtype NonEmptyIntMap a = NonEmptyIntMap {getNonEmptyIntMap :: IntMap a} deriving (Eq, Show)
229236

230237
instance Arbitrary a => Arbitrary (NonEmptyIntMap a) where
231-
arbitrary = fmap (NonEmptyIntMap . fromList . getNonEmpty) arbitrary
238+
arbitrary = oneof [go arbitrary, go (getLarge <$> arbitrary)]
239+
where
240+
go kgen = NonEmptyIntMap . fromList <$> listOf1 ((,) <$> kgen <*> arbitrary)
241+
shrink =
242+
fmap (NonEmptyIntMap . fromList) .
243+
List.filter (not . List.null) .
244+
shrink .
245+
toAscList .
246+
getNonEmptyIntMap
232247

233248

234249
------------------------------------------------------------------------
@@ -1150,6 +1165,10 @@ forValidUnitTree f = forValid f
11501165
prop_valid :: Property
11511166
prop_valid = forValidUnitTree $ \t -> valid t
11521167

1168+
prop_hasPrefix :: Int -> NonZero Int -> Property
1169+
prop_hasPrefix i (NonZero p) =
1170+
hasPrefix i (Prefix p) === hasPrefixSimple i (Prefix p)
1171+
11531172
----------------------------------------------------------------
11541173
-- QuickCheck
11551174
----------------------------------------------------------------
@@ -1641,3 +1660,21 @@ prop_traverseMaybeWithKey_degrade_to_traverseWithKey fun mp =
16411660
-- so this also checks the order of traversing is the same.
16421661
where f k v = (show k, applyFun2 fun k v)
16431662
g k v = fmap Just $ f k v
1663+
1664+
prop_isProperSubmapOfBy :: Fun (A, A) Bool -> IntMap A -> IntMap A -> Property
1665+
prop_isProperSubmapOfBy f m1 m2 =
1666+
isProperSubmapOfBy (applyFun2 f) m1 m2 ===
1667+
(length xs == size m1 && size m1 < size m2)
1668+
where
1669+
xs = List.intersectBy
1670+
(\(k1,x1) (k2,x2) -> k1 == k2 && applyFun2 f x1 x2)
1671+
(assocs m1) (assocs m2)
1672+
1673+
prop_isSubmapOfBy :: Fun (A, A) Bool -> IntMap A -> IntMap A -> Property
1674+
prop_isSubmapOfBy f m1 m2 =
1675+
isSubmapOfBy (applyFun2 f) m1 m2 ===
1676+
(length xs == size m1)
1677+
where
1678+
xs = List.intersectBy
1679+
(\(k1,x1) (k2,x2) -> k1 == k2 && applyFun2 f x1 x2)
1680+
(assocs m1) (assocs m2)

0 commit comments

Comments
 (0)