Skip to content

Commit 596075f

Browse files
authored
Fix space leaks in union[With[Key]] (#380)
Fixes #379.
1 parent f1ea9a4 commit 596075f

File tree

4 files changed

+79
-9
lines changed

4 files changed

+79
-9
lines changed

Data/HashMap/Internal.hs

+3-8
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ module Data.HashMap.Internal
122122
, update32
123123
, update32M
124124
, update32With'
125-
, updateOrConcatWith
126125
, updateOrConcatWithKey
127126
, filterMapAux
128127
, equalKeys
@@ -1551,7 +1550,7 @@ unionWithKey f = go 0
15511550
| h1 == h2 = Collision h1 (updateOrSnocWithKey (\k a b -> (# f k b a #)) k2 v2 ls1)
15521551
| otherwise = goDifferentHash s h1 h2 t1 t2
15531552
go s t1@(Collision h1 ls1) t2@(Collision h2 ls2)
1554-
| h1 == h2 = Collision h1 (updateOrConcatWithKey f ls1 ls2)
1553+
| h1 == h2 = Collision h1 (updateOrConcatWithKey (\k a b -> (# f k a b #)) ls1 ls2)
15551554
| otherwise = goDifferentHash s h1 h2 t1 t2
15561555
-- branch vs. branch
15571556
go s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) =
@@ -2177,11 +2176,7 @@ updateOrSnocWithKey f k0 v0 ary0 = go k0 v0 ary0 0 (A.length ary0)
21772176
= go k v ary (i+1) n
21782177
{-# INLINABLE updateOrSnocWithKey #-}
21792178

2180-
updateOrConcatWith :: Eq k => (v -> v -> v) -> A.Array (Leaf k v) -> A.Array (Leaf k v) -> A.Array (Leaf k v)
2181-
updateOrConcatWith f = updateOrConcatWithKey (const f)
2182-
{-# INLINABLE updateOrConcatWith #-}
2183-
2184-
updateOrConcatWithKey :: Eq k => (k -> v -> v -> v) -> A.Array (Leaf k v) -> A.Array (Leaf k v) -> A.Array (Leaf k v)
2179+
updateOrConcatWithKey :: Eq k => (k -> v -> v -> (# v #)) -> A.Array (Leaf k v) -> A.Array (Leaf k v) -> A.Array (Leaf k v)
21852180
updateOrConcatWithKey f ary1 ary2 = A.run $ do
21862181
-- TODO: instead of mapping and then folding, should we traverse?
21872182
-- We'll have to be careful to avoid allocating pairs or similar.
@@ -2203,7 +2198,7 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do
22032198
Just i1 -> do -- key occurs in both arrays, store combination in position i1
22042199
L k v1 <- A.indexM ary1 i1
22052200
L _ v2 <- A.indexM ary2 i2
2206-
A.write mary i1 (L k (f k v1 v2))
2201+
case f k v1 v2 of (# v3 #) -> A.write mary i1 (L k v3)
22072202
go iEnd (i2+1)
22082203
Nothing -> do -- key is only in ary2, append to end
22092204
A.write mary iEnd =<< A.indexM ary2 i2

Data/HashMap/Internal/Strict.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ unionWithKey f = go 0
453453
| h1 == h2 = Collision h1 (updateOrSnocWithKey (flip . f) k2 v2 ls1)
454454
| otherwise = goDifferentHash s h1 h2 t1 t2
455455
go s t1@(Collision h1 ls1) t2@(Collision h2 ls2)
456-
| h1 == h2 = Collision h1 (updateOrConcatWithKey f ls1 ls2)
456+
| h1 == h2 = Collision h1 (updateOrConcatWithKey (\k a b -> let !v = f k a b in (# v #)) ls1 ls2)
457457
| otherwise = goDifferentHash s h1 h2 t1 t2
458458
-- branch vs. branch
459459
go s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) =

tests/Regressions.hs

+71
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE MagicHash #-}
23
{-# LANGUAGE ScopedTypeVariables #-}
34
{-# LANGUAGE UnboxedTuples #-}
@@ -22,6 +23,13 @@ import Test.Tasty.QuickCheck (testProperty)
2223
import qualified Data.HashMap.Lazy as HML
2324
import qualified Data.HashMap.Strict as HMS
2425

26+
#if MIN_VERSION_base(4,12,0)
27+
-- nothunks requires base >= 4.12
28+
#define HAVE_NOTHUNKS
29+
import qualified Data.Foldable as Foldable
30+
import NoThunks.Class (noThunksInValues)
31+
#endif
32+
2533
issue32 :: Assertion
2634
issue32 = assert $ isJust $ HMS.lookup 7 m'
2735
where
@@ -124,6 +132,61 @@ issue254Strict = do
124132
touch mp
125133
assert $ isNothing res
126134

135+
------------------------------------------------------------------------
136+
-- Issue #379
137+
138+
#ifdef HAVE_NOTHUNKS
139+
140+
issue379Union :: Assertion
141+
issue379Union = do
142+
let m0 = HMS.fromList [(KC 1, ()), (KC 2, ())]
143+
let m1 = HMS.fromList [(KC 2, ()), (KC 3, ())]
144+
let u = m0 `HMS.union` m1
145+
mThunkInfo <- noThunksInValues mempty (Foldable.toList u)
146+
assert $ isNothing mThunkInfo
147+
148+
issue379StrictUnionWith :: Assertion
149+
issue379StrictUnionWith = do
150+
let m0 = HMS.fromList [(KC 1, 10), (KC 2, 20 :: Int)]
151+
let m1 = HMS.fromList [(KC 2, 20), (KC 3, 30)]
152+
let u = HMS.unionWith (+) m0 m1
153+
mThunkInfo <- noThunksInValues mempty (Foldable.toList u)
154+
assert $ isNothing mThunkInfo
155+
156+
issue379StrictUnionWithKey :: Assertion
157+
issue379StrictUnionWithKey = do
158+
let m0 = HMS.fromList [(KC 1, 10), (KC 2, 20 :: Int)]
159+
let m1 = HMS.fromList [(KC 2, 20), (KC 3, 30)]
160+
let u = HMS.unionWithKey (\(KC i) v0 v1 -> i + v0 + v1) m0 m1
161+
mThunkInfo <- noThunksInValues mempty (Foldable.toList u)
162+
assert $ isNothing mThunkInfo
163+
164+
#endif
165+
166+
-- Another key type that always collides.
167+
--
168+
-- Note (sjakobi): The KC newtype of Int somehow can't be used to demonstrate
169+
-- the space leak in issue379LazyUnionWith. This type does the trick.
170+
newtype SC = SC String
171+
deriving (Eq, Ord, Show)
172+
instance Hashable SC where
173+
hashWithSalt salt _ = salt
174+
175+
issue379LazyUnionWith :: Assertion
176+
issue379LazyUnionWith = do
177+
i :: Int <- randomIO
178+
let k = SC (show i)
179+
weakK <- mkWeakPtr k Nothing -- add the ability to test whether k is alive
180+
let f :: Int -> Int
181+
f x = error ("Should not be evaluated " ++ show x)
182+
let m = HML.fromList [(SC "1", f 1), (SC "2", f 2), (k, f 3)]
183+
let u = HML.unionWith (+) m m
184+
Just v <- evaluate $ HML.lookup k u
185+
performGC
186+
res <- deRefWeak weakK -- gives Just if k is still alive
187+
touch v -- makes sure that we didn't GC away the combined value
188+
assert $ isNothing res
189+
127190
------------------------------------------------------------------------
128191
-- * Test list
129192

@@ -135,4 +198,12 @@ tests = testGroup "Regression tests"
135198
, testProperty "issue39b" propEqAfterDelete
136199
, testCase "issue254 lazy" issue254Lazy
137200
, testCase "issue254 strict" issue254Strict
201+
, testGroup "issue379"
202+
[ testCase "Lazy.unionWith" issue379LazyUnionWith
203+
#ifdef HAVE_NOTHUNKS
204+
, testCase "union" issue379Union
205+
, testCase "Strict.unionWith" issue379StrictUnionWith
206+
, testCase "Strict.unionWithKey" issue379StrictUnionWithKey
207+
#endif
208+
]
138209
]

unordered-containers.cabal

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ test-suite unordered-containers-tests
9898
tasty-quickcheck >= 0.10.1.2,
9999
unordered-containers
100100

101+
if impl(ghc >= 8.6)
102+
build-depends:
103+
nothunks >= 0.1.3
104+
101105
default-language: Haskell2010
102106
ghc-options: -Wall
103107
cpp-options: -DASSERTS

0 commit comments

Comments
 (0)