Skip to content

Commit 319d0b9

Browse files
committed
Improve Int{Map,Set} construction from asc lists
* Switch to an explicit stack based approach, like we had before 77c8e5f, and like we have for Set and Map today. * This is a little faster (-13%) on in-memory lists, but much faster (-43%) when list fusion applies because we are now a good consumer. For a dense IntSet in the fusion case it is greatly faster (-81%). * Avoid a case of undefined behavior in IntMap.fromDistinctAscList due to calling branchMask on equal keys if the caller did not follow the distinct precondition. * Add property tests for the functions
1 parent 027ea3c commit 319d0b9

File tree

7 files changed

+250
-249
lines changed

7 files changed

+250
-249
lines changed

containers-tests/benchmarks/IntMap.hs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ main = do
4646
, bench "mapMaybeWithKey" $ whnf (M.mapMaybeWithKey (const maybeDel)) m
4747
, bench "fromList" $ whnf M.fromList elems
4848
, bench "fromAscList" $ whnf M.fromAscList elems
49-
, bench "fromDistinctAscList" $ whnf M.fromDistinctAscList elems
49+
, bench "fromAscList:fusion" $
50+
whnf (\n -> M.fromAscList [(i,()) | i <- [1..n]]) bound
5051
, bench "minView" $ whnf (maybe 0 (\((k,v), m) -> k+v+M.size m) . M.minViewWithKey)
5152
(M.fromList $ zip [1..10] [1..10])
5253
, bench "spanAntitone" $ whnf (M.spanAntitone (<key_mid)) m
@@ -61,17 +62,18 @@ main = do
6162
where
6263
elems = elems_hits
6364
elems_hits = zip keys values
64-
elems_mid = zip (map (+ (2^12 `div` 2)) keys) values
65-
elems_most = zip (map (+ (2^12 `div` 10)) keys) values
65+
elems_mid = zip (map (+ (bound `div` 2)) keys) values
66+
elems_most = zip (map (+ (bound `div` 10)) keys) values
6667
elems_misses = zip (map (\x-> x * 2 + 1) keys) values
6768
elems_mixed = zip mixedKeys values
6869
--------------------------------------------------------
69-
keys = [1..2^12]
70+
bound = 2^12
71+
keys = [1..bound]
7072
keys' = fmap (+ 1000000) keys
71-
keys'' = fmap (* 2) [1..2^12]
73+
keys'' = fmap (* 2) [1..bound]
7274
mixedKeys = interleave keys keys'
73-
values = [1..2^12]
74-
key_mid = 2^11
75+
values = [1..bound]
76+
key_mid = bound `div` 2
7577
--------------------------------------------------------
7678
sum k v1 v2 = k + v1 + v2
7779
consPair k v xs = (k, v) : xs

containers-tests/benchmarks/IntSet.hs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ main = do
4141
, bench "fromList" $ whnf IS.fromList elems
4242
, bench "fromRange" $ whnf IS.fromRange (1,bound)
4343
, bench "fromRange:small" $ whnf IS.fromRange (-1,0)
44-
, bench "fromAscList" $ whnf IS.fromAscList elems
45-
, bench "fromDistinctAscList" $ whnf IS.fromDistinctAscList elems
44+
, bench "fromAscList" $ whnf fromAscListNoinline elems
45+
, bench "fromAscList:fusion" $ whnf (\n -> IS.fromAscList [1..n]) bound
46+
, bench "fromAscList:sparse" $ whnf fromAscListNoinline elems_sparse
47+
, bench "fromAscList:sparse:fusion" $
48+
whnf (\n -> IS.fromAscList (map (*64) [1..n])) bound
4649
, bench "disjoint:false" $ whnf (IS.disjoint s) s_even
4750
, bench "disjoint:true" $ whnf (IS.disjoint s_odd) s_even
4851
, bench "null.intersection:false" $ whnf (IS.null. IS.intersection s) s_even
@@ -81,6 +84,11 @@ ins xs s0 = foldl' (\s a -> IS.insert a s) s0 xs
8184
del :: [Int] -> IS.IntSet -> IS.IntSet
8285
del xs s0 = foldl' (\s k -> IS.delete k s) s0 xs
8386

87+
-- NOINLINE to work around an issue where the inlined function doesn't get
88+
-- optimized (see GHC #25878).
89+
fromAscListNoinline :: [Int] -> IS.IntSet
90+
fromAscListNoinline = IS.fromAscList
91+
{-# NOINLINE fromAscListNoinline #-}
8492

8593

8694
-- | Automata contain just the transitions

containers-tests/tests/intmap-properties.hs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import qualified Prelude (map, filter)
2929

3030
import Data.List (nub,sort)
3131
import qualified Data.List as List
32+
import qualified Data.List.NonEmpty as NE
3233
import qualified Data.IntSet as IntSet
3334
import Test.Tasty
3435
import Test.Tasty.HUnit
@@ -157,7 +158,6 @@ main = defaultMain $ testGroup "intmap-properties"
157158
, testProperty "mergeWithKey model" prop_mergeWithKeyModel
158159
, testProperty "merge valid" prop_merge_valid
159160
, testProperty "mergeA effects" prop_mergeA_effects
160-
, testProperty "fromAscList" prop_ordered
161161
, testProperty "fromList then toList" prop_list
162162
, testProperty "toDescList" prop_descList
163163
, testProperty "toAscList+toDescList" prop_ascDescList
@@ -248,6 +248,10 @@ main = defaultMain $ testGroup "intmap-properties"
248248
, testProperty "mapKeysWith" prop_mapKeysWith
249249
, testProperty "mapKeysMonotonic" prop_mapKeysMonotonic
250250
, testProperty "compare" prop_compare
251+
, testProperty "fromAscList" prop_fromAscList
252+
, testProperty "fromAscListWith" prop_fromAscListWith
253+
, testProperty "fromAscListWithKey" prop_fromAscListWithKey
254+
, testProperty "fromDistinctAscList" prop_fromDistinctAscList
251255
]
252256

253257
{--------------------------------------------------------------------
@@ -1402,12 +1406,6 @@ prop_mergeA_effects xs ys
14021406

14031407
----------------------------------------------------------------
14041408

1405-
prop_ordered :: Property
1406-
prop_ordered
1407-
= forAll (choose (5,100)) $ \n ->
1408-
let xs = [(x,()) | x <- [0..n::Int]]
1409-
in fromAscList xs == fromList xs
1410-
14111409
prop_list :: [Int] -> Bool
14121410
prop_list xs = (sort (nub xs) == [x | (x,()) <- toList (fromList [(x,()) | x <- xs])])
14131411

@@ -1984,3 +1982,38 @@ prop_mapKeysMonotonic (Positive a) b m =
19841982

19851983
prop_compare :: IntMap OrdA -> IntMap OrdA -> Property
19861984
prop_compare m1 m2 = compare m1 m2 === compare (toList m1) (toList m2)
1985+
1986+
prop_fromAscList :: [(Int, A)] -> Property
1987+
prop_fromAscList kxs =
1988+
valid t .&&.
1989+
t === fromList sortedKxs
1990+
where
1991+
sortedKxs = List.sortBy (comparing fst) kxs
1992+
t = fromAscList sortedKxs
1993+
1994+
prop_fromAscListWith :: Fun (A, A) A -> [(Int, A)] -> Property
1995+
prop_fromAscListWith f kxs =
1996+
valid t .&&.
1997+
t === fromListWith (applyFun2 f) sortedKxs
1998+
where
1999+
sortedKxs = List.sortBy (comparing fst) kxs
2000+
t = fromAscListWith (applyFun2 f) sortedKxs
2001+
2002+
prop_fromAscListWithKey :: Fun (Int, A, A) A -> [(Int, A)] -> Property
2003+
prop_fromAscListWithKey f kxs =
2004+
valid t .&&.
2005+
t === fromListWithKey (applyFun3 f) sortedKxs
2006+
where
2007+
sortedKxs = List.sortBy (comparing fst) kxs
2008+
t = fromAscListWithKey (applyFun3 f) sortedKxs
2009+
2010+
prop_fromDistinctAscList :: [(Int, A)] -> Property
2011+
prop_fromDistinctAscList kxs =
2012+
valid t .&&.
2013+
toList t === nubSortedKxs
2014+
where
2015+
nubSortedKxs =
2016+
List.map NE.head $
2017+
NE.groupBy ((==) `on` fst) $
2018+
List.sortBy (comparing fst) kxs
2019+
t = fromDistinctAscList nubSortedKxs

containers-tests/tests/intset-properties.hs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ main = defaultMain $ testGroup "intset-properties"
4646
, testProperty "prop_difference" prop_difference
4747
, testProperty "prop_intersection" prop_intersection
4848
, testProperty "prop_symmetricDifference" prop_symmetricDifference
49-
, testProperty "prop_Ordered" prop_Ordered
5049
, testProperty "prop_List" prop_List
5150
, testProperty "prop_DescList" prop_DescList
5251
, testProperty "prop_AscDescList" prop_AscDescList
@@ -90,6 +89,8 @@ main = defaultMain $ testGroup "intset-properties"
9089
, testProperty "delete" prop_delete
9190
, testProperty "deleteMin" prop_deleteMin
9291
, testProperty "deleteMax" prop_deleteMax
92+
, testProperty "fromAscList" prop_fromAscList
93+
, testProperty "fromDistinctAscList" prop_fromDistinctAscList
9394
]
9495

9596
----------------------------------------------------------------
@@ -281,10 +282,6 @@ prop_disjoint a b = a `disjoint` b == null (a `intersection` b)
281282
{--------------------------------------------------------------------
282283
Lists
283284
--------------------------------------------------------------------}
284-
prop_Ordered
285-
= forAll (choose (5,100)) $ \n ->
286-
let xs = concat [[i-n,i-n]|i<-[0..2*n :: Int]]
287-
in fromAscList xs == fromList xs
288285

289286
prop_List :: [Int] -> Bool
290287
prop_List xs
@@ -521,3 +518,20 @@ prop_deleteMin s = toList (deleteMin s) === if null s then [] else tail (toList
521518

522519
prop_deleteMax :: IntSet -> Property
523520
prop_deleteMax s = toList (deleteMax s) === if null s then [] else init (toList s)
521+
522+
prop_fromAscList :: [Int] -> Property
523+
prop_fromAscList xs =
524+
valid t .&&.
525+
toList t === nubSortedXs
526+
where
527+
sortedXs = sort xs
528+
nubSortedXs = List.map NE.head $ NE.group sortedXs
529+
t = fromAscList sortedXs
530+
531+
prop_fromDistinctAscList :: [Int] -> Property
532+
prop_fromDistinctAscList xs =
533+
valid t .&&.
534+
toList t === nubSortedXs
535+
where
536+
nubSortedXs = List.map NE.head $ NE.group $ sort xs
537+
t = fromDistinctAscList nubSortedXs

containers/src/Data/IntMap/Internal.hs

Lines changed: 91 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ module Data.IntMap.Internal (
290290
, bin
291291
, binCheckLeft
292292
, binCheckRight
293+
, MonoState(..)
294+
, Stack(..)
295+
, ascLinkTop
296+
, ascLinkAll
293297

294298
-- * Used by "IntMap.Merge.Lazy" and "IntMap.Merge.Strict"
295299
, mapWhenMissing
@@ -3385,8 +3389,8 @@ fromListWithKey f xs
33853389
-- > fromAscList [(3,"b"), (5,"a"), (5,"b")] == fromList [(3, "b"), (5, "b")]
33863390

33873391
fromAscList :: [(Key,a)] -> IntMap a
3388-
fromAscList = fromMonoListWithKey Nondistinct (\_ x _ -> x)
3389-
{-# NOINLINE fromAscList #-}
3392+
fromAscList xs = fromAscListWithKey (\_ x _ -> x) xs
3393+
{-# INLINE fromAscList #-} -- Inline for list fusion
33903394

33913395
-- | \(O(n)\). Build a map from a list of key\/value pairs where
33923396
-- the keys are in ascending order, with a combining function on equal keys.
@@ -3400,8 +3404,8 @@ fromAscList = fromMonoListWithKey Nondistinct (\_ x _ -> x)
34003404
-- Also see the performance note on 'fromListWith'.
34013405

34023406
fromAscListWith :: (a -> a -> a) -> [(Key,a)] -> IntMap a
3403-
fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
3404-
{-# NOINLINE fromAscListWith #-}
3407+
fromAscListWith f xs = fromAscListWithKey (\_ x y -> f x y) xs
3408+
{-# INLINE fromAscListWith #-} -- Inline for list fusion
34053409

34063410
-- | \(O(n)\). Build a map from a list of key\/value pairs where
34073411
-- the keys are in ascending order, with a combining function on equal keys.
@@ -3415,86 +3419,66 @@ fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
34153419
--
34163420
-- Also see the performance note on 'fromListWith'.
34173421

3422+
-- See Note [fromAscList implementation]
34183423
fromAscListWithKey :: (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
3419-
fromAscListWithKey f = fromMonoListWithKey Nondistinct f
3420-
{-# NOINLINE fromAscListWithKey #-}
3424+
fromAscListWithKey f xs = ascLinkAll (Foldable.foldl' next MSNada xs)
3425+
where
3426+
next s (!ky, y) = case s of
3427+
MSNada -> MSPush ky y Nada
3428+
MSPush kx x stk
3429+
| kx == ky -> MSPush ky (f ky y x) stk
3430+
| otherwise -> let m = branchMask kx ky
3431+
in MSPush ky y (ascLinkTop stk kx (Tip kx x) m)
3432+
{-# INLINE fromAscListWithKey #-} -- Inline for list fusion
34213433

34223434
-- | \(O(n)\). Build a map from a list of key\/value pairs where
34233435
-- the keys are in ascending order and all distinct.
34243436
--
3425-
-- __Warning__: This function should be used only if the keys are in
3426-
-- strictly increasing order. This precondition is not checked. Use 'fromList'
3427-
-- if the precondition may not hold.
3428-
--
3429-
-- > fromDistinctAscList [(3,"b"), (5,"a")] == fromList [(3, "b"), (5, "a")]
3430-
3431-
fromDistinctAscList :: [(Key,a)] -> IntMap a
3432-
fromDistinctAscList = fromMonoListWithKey Distinct (\_ x _ -> x)
3433-
{-# NOINLINE fromDistinctAscList #-}
3434-
3435-
-- | \(O(n)\). Build a map from a list of key\/value pairs with monotonic keys
3436-
-- and a combining function.
3437+
-- @fromDistinctAscList = 'fromAscList'@
34373438
--
3438-
-- The precise conditions under which this function works are subtle:
3439-
-- For any branch mask, keys with the same prefix w.r.t. the branch
3440-
-- mask must occur consecutively in the list.
3439+
-- See warning on 'fromAscList'.
34413440
--
3442-
-- Also see the performance note on 'fromListWith'.
3443-
3444-
fromMonoListWithKey :: Distinct -> (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
3445-
fromMonoListWithKey distinct f = go
3446-
where
3447-
go [] = Nil
3448-
go ((kx,vx) : zs1) = addAll' kx vx zs1
3449-
3450-
-- `addAll'` collects all keys equal to `kx` into a single value,
3451-
-- and then proceeds with `addAll`.
3452-
addAll' !kx vx []
3453-
= Tip kx vx
3454-
addAll' !kx vx ((ky,vy) : zs)
3455-
| Nondistinct <- distinct, kx == ky
3456-
= let v = f kx vy vx in addAll' ky v zs
3457-
-- inlined: | otherwise = addAll kx (Tip kx vx) (ky : zs)
3458-
| m <- branchMask kx ky
3459-
, Inserted ty zs' <- addMany' m ky vy zs
3460-
= addAll kx (linkWithMask m ky ty kx (Tip kx vx)) zs'
3461-
3462-
-- for `addAll` and `addMany`, kx is /a/ key inside the tree `tx`
3463-
-- `addAll` consumes the rest of the list, adding to the tree `tx`
3464-
addAll !_kx !tx []
3465-
= tx
3466-
addAll !kx !tx ((ky,vy) : zs)
3467-
| m <- branchMask kx ky
3468-
, Inserted ty zs' <- addMany' m ky vy zs
3469-
= addAll kx (linkWithMask m ky ty kx tx) zs'
3470-
3471-
-- `addMany'` is similar to `addAll'`, but proceeds with `addMany'`.
3472-
addMany' !_m !kx vx []
3473-
= Inserted (Tip kx vx) []
3474-
addMany' !m !kx vx zs0@((ky,vy) : zs)
3475-
| Nondistinct <- distinct, kx == ky
3476-
= let v = f kx vy vx in addMany' m ky v zs
3477-
-- inlined: | otherwise = addMany m kx (Tip kx vx) (ky : zs)
3478-
| mask kx m /= mask ky m
3479-
= Inserted (Tip kx vx) zs0
3480-
| mxy <- branchMask kx ky
3481-
, Inserted ty zs' <- addMany' mxy ky vy zs
3482-
= addMany m kx (linkWithMask mxy ky ty kx (Tip kx vx)) zs'
3483-
3484-
-- `addAll` adds to `tx` all keys whose prefix w.r.t. `m` agrees with `kx`.
3485-
addMany !_m !_kx tx []
3486-
= Inserted tx []
3487-
addMany !m !kx tx zs0@((ky,vy) : zs)
3488-
| mask kx m /= mask ky m
3489-
= Inserted tx zs0
3490-
| mxy <- branchMask kx ky
3491-
, Inserted ty zs' <- addMany' mxy ky vy zs
3492-
= addMany m kx (linkWithMask mxy ky ty kx tx) zs'
3493-
{-# INLINE fromMonoListWithKey #-}
3494-
3495-
data Inserted a = Inserted !(IntMap a) ![(Key,a)]
3496-
3497-
data Distinct = Distinct | Nondistinct
3441+
-- This definition exists for backwards compatibility. It offers no advantage
3442+
-- over @fromAscList@.
3443+
fromDistinctAscList :: [(Key,a)] -> IntMap a
3444+
-- Note: There is nothing we can optimize compared to fromAscList.
3445+
-- The adjacent key equals check (kx == ky) might seem unnecessary for
3446+
-- fromDistinctAscList, but it guards branchMask which has undefined behavior
3447+
-- under that case. We could error on kx == ky instead, but that isn't any
3448+
-- better.
3449+
fromDistinctAscList = fromAscList
3450+
{-# INLINE fromDistinctAscList #-} -- Inline for list fusion
3451+
3452+
data Stack a
3453+
= Nada
3454+
| Push {-# UNPACK #-} !Int !(IntMap a) !(Stack a)
3455+
3456+
data MonoState a
3457+
= MSNada
3458+
| MSPush {-# UNPACK #-} !Key a !(Stack a)
3459+
3460+
ascLinkTop :: Stack a -> Int -> IntMap a -> Int -> Stack a
3461+
ascLinkTop stk !rk r !rm = case stk of
3462+
Nada -> Push rm r stk
3463+
Push m l stk'
3464+
| i2w m < i2w rm -> let p = Prefix (mask rk m .|. m)
3465+
in ascLinkTop stk' rk (Bin p l r) rm
3466+
| otherwise -> Push rm r stk
3467+
3468+
ascLinkAll :: MonoState a -> IntMap a
3469+
ascLinkAll s = case s of
3470+
MSNada -> Nil
3471+
MSPush kx x stk -> ascLinkStack stk kx (Tip kx x)
3472+
{-# INLINABLE ascLinkAll #-}
3473+
3474+
ascLinkStack :: Stack a -> Int -> IntMap a -> IntMap a
3475+
ascLinkStack stk !rk r = case stk of
3476+
Nada -> r
3477+
Push m l stk'
3478+
| signBranch p -> Bin p r l
3479+
| otherwise -> ascLinkStack stk' rk (Bin p l r)
3480+
where
3481+
p = Prefix (mask rk m .|. m)
34983482

34993483
{--------------------------------------------------------------------
35003484
Eq
@@ -3884,3 +3868,33 @@ withEmpty bars = " ":bars
38843868
-- * This is similar to the Map merge complexity, which is O(m log (n/m)).
38853869
-- * When m is a small constant the term simplifies to O(min(n, W)), which is
38863870
-- just the complexity we expect for single operations like insert and delete.
3871+
3872+
-- Note [fromAscList implementation]
3873+
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3874+
-- fromAscList is an implementation that builds up the result bottom-up
3875+
-- in linear time. It maintains a state (MonoState) that gets updated with
3876+
-- key-value pairs from the input list one at a time. The state contains the
3877+
-- last key-value pair, and a stack of pending trees.
3878+
--
3879+
-- For a new key-value pair, the branchMask with the previous key is computed.
3880+
-- This represents the depth of the lowest common ancestor that the tree with
3881+
-- the previous key, say tl, and the tree with the new key, tr, must have in
3882+
-- the final result. Since the keys are in ascending order we expect no more
3883+
-- keys in tl, and we can build it by moving up the stack and linking trees. We
3884+
-- know when to stop by the branchMask value. We must not link higher than that
3885+
-- depth, otherwise instead of tl we will build the parent of tl prematurely
3886+
-- before tr is ready. Once the linking is done, tl will be at the top of the
3887+
-- stack.
3888+
--
3889+
-- We also store the branchMask of a tree with its future right sibling in the
3890+
-- stack. This is an optimization, benchmarks show that this is faster than
3891+
-- recomputing the branchMask values when linking trees.
3892+
--
3893+
-- In the end, we link all the trees remaining in the stack. There is a small
3894+
-- catch: negative keys appear in the input before non-negative keys (if they
3895+
-- both appear), but the tree with negative keys and the tree with non-negative
3896+
-- keys must be the right and left child of the root respectively. So we check
3897+
-- for this and link them accordingly.
3898+
--
3899+
-- The implementation is defined as a foldl' over the input list, which makes
3900+
-- it a good consumer in list fusion.

0 commit comments

Comments
 (0)