Skip to content

Commit

Permalink
Map/IntMap: Document the most common O(n²) performance mistake (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
nh2 committed Sep 27, 2023
1 parent 269f53e commit 22f2247
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 6 deletions.
4 changes: 4 additions & 0 deletions containers/docs/map.rst
Expand Up @@ -202,6 +202,10 @@ values with the same key.
Map.fromListWith (++) [(1, "a"), (1, "b"), (2, "x"), (2, "y")]
> fromList [(1,"ba"),(2,"yx")]

.. TIP::
Read the documentation of :haddock_short:`/Data.Map.Strict#fromListWith`
to avoid accidental ``O(n²)`` performance when writing patterns
such as ``fromListWith (++)``.


Create a list from a map
Expand Down
55 changes: 53 additions & 2 deletions containers/src/Data/IntMap/Internal.hs
Expand Up @@ -830,6 +830,8 @@ insert k x Nil = Tip k x
-- > insertWith (++) 5 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "xxxa")]
-- > insertWith (++) 7 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "a"), (7, "xxx")]
-- > insertWith (++) 5 "xxx" empty == singleton 5 "xxx"
--
-- Also see the performance note on 'fromListWith'.

insertWith :: (a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWith f k x t
Expand All @@ -845,6 +847,8 @@ insertWith f k x t
-- > insertWithKey f 5 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "5:xxx|a")]
-- > insertWithKey f 7 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "a"), (7, "xxx")]
-- > insertWithKey f 5 "xxx" empty == singleton 5 "xxx"
--
-- Also see the performance note on 'fromListWith'.

insertWithKey :: (Key -> a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWithKey f !k x t@(Bin p m l r)
Expand All @@ -870,6 +874,8 @@ insertWithKey _ k x Nil = Tip k x
-- > let insertLookup kx x t = insertLookupWithKey (\_ a _ -> a) kx x t
-- > insertLookup 5 "x" (fromList [(5,"a"), (3,"b")]) == (Just "a", fromList [(3, "b"), (5, "x")])
-- > insertLookup 7 "x" (fromList [(5,"a"), (3,"b")]) == (Nothing, fromList [(3, "b"), (5, "a"), (7, "x")])
--
-- Also see the performance note on 'fromListWith'.

insertLookupWithKey :: (Key -> a -> a -> a) -> Key -> a -> IntMap a -> (Maybe a, IntMap a)
insertLookupWithKey f !k x t@(Bin p m l r)
Expand Down Expand Up @@ -1085,6 +1091,8 @@ union m1 m2
-- | \(O(n+m)\). The union with a combining function.
--
-- > unionWith (++) (fromList [(5, "a"), (3, "b")]) (fromList [(5, "A"), (7, "C")]) == fromList [(3, "b"), (5, "aA"), (7, "C")]
--
-- Also see the performance note on 'fromListWith'.

unionWith :: (a -> a -> a) -> IntMap a -> IntMap a -> IntMap a
unionWith f m1 m2
Expand All @@ -1094,6 +1102,8 @@ unionWith f m1 m2
--
-- > let f key left_value right_value = (show key) ++ ":" ++ left_value ++ "|" ++ right_value
-- > unionWithKey f (fromList [(5, "a"), (3, "b")]) (fromList [(5, "A"), (7, "C")]) == fromList [(3, "b"), (5, "5:a|A"), (7, "C")]
--
-- Also see the performance note on 'fromListWith'.

unionWithKey :: (Key -> a -> a -> a) -> IntMap a -> IntMap a -> IntMap a
unionWithKey f m1 m2
Expand Down Expand Up @@ -2540,6 +2550,8 @@ mapKeys f = fromList . foldrWithKey (\k x xs -> (f k, x) : xs) []
--
-- > mapKeysWith (++) (\ _ -> 1) (fromList [(1,"b"), (2,"a"), (3,"d"), (4,"c")]) == singleton 1 "cdab"
-- > mapKeysWith (++) (\ _ -> 3) (fromList [(1,"b"), (2,"a"), (3,"d"), (4,"c")]) == singleton 3 "cdab"
--
-- Also see the performance note on 'fromListWith'.

mapKeysWith :: (a -> a -> a) -> (Key->Key) -> IntMap a -> IntMap a
mapKeysWith c f
Expand Down Expand Up @@ -3195,10 +3207,41 @@ fromList xs
where
ins t (k,x) = insert k x t

-- | \(O(n \min(n,W))\). Create a map from a list of key\/value pairs with a combining function. See also 'fromAscListWith'.
-- | \(O(n \min(n,W))\). Build a map from a list of key\/value pairs with a combining function. See also 'fromAscListWith'.
--
-- > fromListWith (++) [(5,"a"), (5,"b"), (3,"b"), (3,"a"), (5,"c")] == fromList [(3, "ab"), (5, "cba")]
-- > fromListWith (++) [(5,"a"), (5,"b"), (3,"x"), (5,"c")] == fromList [(3, "x"), (5, "cba")]
-- > fromListWith (++) [] == empty
--
-- Note the reverse ordering of @"cba"@ in the example.
--
-- The symmetric combining function @f@ is applied in a left-fold over the list, as @f new old@.
--
-- === Performance
--
-- You should ensure that the given @f@ is fast with this order of arguments.
--
-- Symmetric functions may be slow in one order, and fast in another.
-- For the common case of collecting values of matching keys in a list, as above:
--
-- The complexity of @(++) a b@ is \(O(a)\), so it is fast when given a short list as its first argument.
-- Thus:
--
-- > fromListWith (++) (replicate 1000000 (3, "x")) -- O(n), fast
-- > fromListWith (flip (++)) (replicate 1000000 (3, "x")) -- O(n²), extremely slow
--
-- because they evaluate as, respectively:
--
-- > fromList [(3, "x" ++ ("x" ++ "xxxxx..xxxxx"))] -- O(n)
-- > fromList [(3, ("xxxxx..xxxxx" ++ "x") ++ "x")] -- O(n²)
--
-- Thus, to get good performance with an operation like @(++)@ while also preserving
-- the same order as in the input list, reverse the input:
--
-- > fromListWith (++) (reverse [(5,"a"), (5,"b"), (5,"c")]) == fromList [(5, "abc")]
--
-- and it is always fast to combine singleton-list values @[v]@ with @fromListWith (++)@, as in:
--
-- > fromListWith (++) $ reverse $ map (\(k, v) -> (k, [v])) someListOfTuples

fromListWith :: (a -> a -> a) -> [(Key,a)] -> IntMap a
fromListWith f xs
Expand All @@ -3209,6 +3252,8 @@ fromListWith f xs
-- > let f key new_value old_value = show key ++ ":" ++ new_value ++ "|" ++ old_value
-- > fromListWithKey f [(5,"a"), (5,"b"), (3,"b"), (3,"a"), (5,"c")] == fromList [(3, "3:a|b"), (5, "5:c|5:b|a")]
-- > fromListWithKey f [] == empty
--
-- Also see the performance note on 'fromListWith'.

fromListWithKey :: (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromListWithKey f xs
Expand All @@ -3231,6 +3276,8 @@ fromAscList = fromMonoListWithKey Nondistinct (\_ x _ -> x)
-- /The precondition (input list is ascending) is not checked./
--
-- > fromAscListWith (++) [(3,"b"), (5,"a"), (5,"b")] == fromList [(3, "b"), (5, "ba")]
--
-- Also see the performance note on 'fromListWith'.

fromAscListWith :: (a -> a -> a) -> [(Key,a)] -> IntMap a
fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
Expand All @@ -3242,6 +3289,8 @@ fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
--
-- > let f key new_value old_value = (show key) ++ ":" ++ new_value ++ "|" ++ old_value
-- > fromAscListWithKey f [(3,"b"), (5,"a"), (5,"b")] == fromList [(3, "b"), (5, "5:b|a")]
--
-- Also see the performance note on 'fromListWith'.

fromAscListWithKey :: (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromAscListWithKey f = fromMonoListWithKey Nondistinct f
Expand All @@ -3263,6 +3312,8 @@ fromDistinctAscList = fromMonoListWithKey Distinct (\_ x _ -> x)
-- The precise conditions under which this function works are subtle:
-- For any branch mask, keys with the same prefix w.r.t. the branch
-- mask must occur consecutively in the list.
--
-- Also see the performance note on 'fromListWith'.

fromMonoListWithKey :: Distinct -> (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromMonoListWithKey distinct f = go
Expand Down
55 changes: 53 additions & 2 deletions containers/src/Data/IntMap/Strict/Internal.hs
Expand Up @@ -425,6 +425,8 @@ insert !k !x t =
-- > insertWith (++) 5 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "xxxa")]
-- > insertWith (++) 7 "xxx" (fromList [(5,"a"), (3,"b")]) == fromList [(3, "b"), (5, "a"), (7, "xxx")]
-- > insertWith (++) 5 "xxx" empty == singleton 5 "xxx"
--
-- Also see the performance note on 'fromListWith'.

insertWith :: (a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWith f k x t
Expand All @@ -443,6 +445,8 @@ insertWith f k x t
--
-- If the key exists in the map, this function is lazy in @value@ but strict
-- in the result of @f@.
--
-- Also see the performance note on 'fromListWith'.

insertWithKey :: (Key -> a -> a -> a) -> Key -> a -> IntMap a -> IntMap a
insertWithKey f !k x t =
Expand Down Expand Up @@ -470,6 +474,8 @@ insertWithKey f !k x t =
-- > let insertLookup kx x t = insertLookupWithKey (\_ a _ -> a) kx x t
-- > insertLookup 5 "x" (fromList [(5,"a"), (3,"b")]) == (Just "a", fromList [(3, "b"), (5, "x")])
-- > insertLookup 7 "x" (fromList [(5,"a"), (3,"b")]) == (Nothing, fromList [(3, "b"), (5, "a"), (7, "x")])
--
-- Also see the performance note on 'fromListWith'.

insertLookupWithKey :: (Key -> a -> a -> a) -> Key -> a -> IntMap a -> (Maybe a, IntMap a)
insertLookupWithKey f0 !k0 x0 t0 = toPair $ go f0 k0 x0 t0
Expand Down Expand Up @@ -660,6 +666,8 @@ unionsWith f ts
-- | \(O(n+m)\). The union with a combining function.
--
-- > unionWith (++) (fromList [(5, "a"), (3, "b")]) (fromList [(5, "A"), (7, "C")]) == fromList [(3, "b"), (5, "aA"), (7, "C")]
--
-- Also see the performance note on 'fromListWith'.

unionWith :: (a -> a -> a) -> IntMap a -> IntMap a -> IntMap a
unionWith f m1 m2
Expand All @@ -669,6 +677,8 @@ unionWith f m1 m2
--
-- > let f key left_value right_value = (show key) ++ ":" ++ left_value ++ "|" ++ right_value
-- > unionWithKey f (fromList [(5, "a"), (3, "b")]) (fromList [(5, "A"), (7, "C")]) == fromList [(3, "b"), (5, "5:a|A"), (7, "C")]
--
-- Also see the performance note on 'fromListWith'.

unionWithKey :: (Key -> a -> a -> a) -> IntMap a -> IntMap a -> IntMap a
unionWithKey f m1 m2
Expand Down Expand Up @@ -986,6 +996,8 @@ mapAccumRWithKey f0 a0 t0 = toPair $ go f0 a0 t0
--
-- > mapKeysWith (++) (\ _ -> 1) (fromList [(1,"b"), (2,"a"), (3,"d"), (4,"c")]) == singleton 1 "cdab"
-- > mapKeysWith (++) (\ _ -> 3) (fromList [(1,"b"), (2,"a"), (3,"d"), (4,"c")]) == singleton 3 "cdab"
--
-- Also see the performance note on 'fromListWith'.

mapKeysWith :: (a -> a -> a) -> (Key->Key) -> IntMap a -> IntMap a
mapKeysWith c f = fromListWith c . foldrWithKey (\k x xs -> (f k, x) : xs) []
Expand Down Expand Up @@ -1095,10 +1107,41 @@ fromList xs
where
ins t (k,x) = insert k x t

-- | \(O(n \min(n,W))\). Create a map from a list of key\/value pairs with a combining function. See also 'fromAscListWith'.
-- | \(O(n \min(n,W))\). Build a map from a list of key\/value pairs with a combining function. See also 'fromAscListWith'.
--
-- > fromListWith (++) [(5,"a"), (5,"b"), (3,"b"), (3,"a"), (5,"a")] == fromList [(3, "ab"), (5, "aba")]
-- > fromListWith (++) [(5,"a"), (5,"b"), (3,"x"), (5,"c")] == fromList [(3, "x"), (5, "cba")]
-- > fromListWith (++) [] == empty
--
-- Note the reverse ordering of @"cba"@ in the example.
--
-- The symmetric combining function @f@ is applied in a left-fold over the list, as @f new old@.
--
-- === Performance
--
-- You should ensure that the given @f@ is fast with this order of arguments.
--
-- Symmetric functions may be slow in one order, and fast in another.
-- For the common case of collecting values of matching keys in a list, as above:
--
-- The complexity of @(++) a b@ is \(O(a)\), so it is fast when given a short list as its first argument.
-- Thus:
--
-- > fromListWith (++) (replicate 1000000 (3, "x")) -- O(n), fast
-- > fromListWith (flip (++)) (replicate 1000000 (3, "x")) -- O(n²), extremely slow
--
-- because they evaluate as, respectively:
--
-- > fromList [(3, "x" ++ ("x" ++ "xxxxx..xxxxx"))] -- O(n)
-- > fromList [(3, ("xxxxx..xxxxx" ++ "x") ++ "x")] -- O(n²)
--
-- Thus, to get good performance with an operation like @(++)@ while also preserving
-- the same order as in the input list, reverse the input:
--
-- > fromListWith (++) (reverse [(5,"a"), (5,"b"), (5,"c")]) == fromList [(5, "abc")]
--
-- and it is always fast to combine singleton-list values @[v]@ with @fromListWith (++)@, as in:
--
-- > fromListWith (++) $ reverse $ map (\(k, v) -> (k, [v])) someListOfTuples

fromListWith :: (a -> a -> a) -> [(Key,a)] -> IntMap a
fromListWith f xs
Expand All @@ -1109,6 +1152,8 @@ fromListWith f xs
-- > let f key new_value old_value = show key ++ ":" ++ new_value ++ "|" ++ old_value
-- > fromListWithKey f [(5,"a"), (5,"b"), (3,"b"), (3,"a"), (5,"c")] == fromList [(3, "3:a|b"), (5, "5:c|5:b|a")]
-- > fromListWithKey f [] == empty
--
-- Also see the performance note on 'fromListWith'.

fromListWithKey :: (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromListWithKey f xs
Expand All @@ -1131,6 +1176,8 @@ fromAscList = fromMonoListWithKey Nondistinct (\_ x _ -> x)
-- /The precondition (input list is ascending) is not checked./
--
-- > fromAscListWith (++) [(3,"b"), (5,"a"), (5,"b")] == fromList [(3, "b"), (5, "ba")]
--
-- Also see the performance note on 'fromListWith'.

fromAscListWith :: (a -> a -> a) -> [(Key,a)] -> IntMap a
fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
Expand All @@ -1141,6 +1188,8 @@ fromAscListWith f = fromMonoListWithKey Nondistinct (\_ x y -> f x y)
-- /The precondition (input list is ascending) is not checked./
--
-- > fromAscListWith (++) [(3,"b"), (5,"a"), (5,"b")] == fromList [(3, "b"), (5, "ba")]
--
-- Also see the performance note on 'fromListWith'.

fromAscListWithKey :: (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromAscListWithKey f = fromMonoListWithKey Nondistinct f
Expand All @@ -1162,6 +1211,8 @@ fromDistinctAscList = fromMonoListWithKey Distinct (\_ x _ -> x)
-- The precise conditions under which this function works are subtle:
-- For any branch mask, keys with the same prefix w.r.t. the branch
-- mask must occur consecutively in the list.
--
-- Also see the performance note on 'fromListWith'.

fromMonoListWithKey :: Distinct -> (Key -> a -> a -> a) -> [(Key,a)] -> IntMap a
fromMonoListWithKey distinct f = go
Expand Down

0 comments on commit 22f2247

Please sign in to comment.