Skip to content

Commit 84cd6ae

Browse files
authored
Improve performance of nub{Ord,Int}On after fusion (#1206)
INLINE the FB functions and use oneShot. Improves benchmark times on GHC 9.14: nubInt.issue1202_distinct -45% nubInt.issue1202_repeat -76% nubOrd.issue1202_distinct -8% nubOrd.issue1202_repeat -60%
1 parent 8d8c55b commit 84cd6ae

3 files changed

Lines changed: 83 additions & 17 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
3+
import Control.DeepSeq (rnf)
4+
import Control.Exception (evaluate)
5+
import qualified Data.Containers.ListUtils as LU
6+
7+
import Test.Tasty.Bench (bench, bgroup, defaultMain, whnf)
8+
9+
main :: IO ()
10+
main = do
11+
evaluate $ rnf [xs_distinct, xs_repeat]
12+
evaluate $ rnf [xss_distinct, xss_repeat]
13+
defaultMain
14+
[ bgroup "nubOrd"
15+
[ bench "no_fusion_distinct" $
16+
whnf (consumeNoFusion . LU.nubOrd) xs_distinct
17+
, bench "no_fusion_repeat" $
18+
whnf (consumeNoFusion . LU.nubOrd) xs_repeat
19+
, bench "issue1202_distinct" $
20+
whnf (consumeNoFusion . collectFrameworksDirs) xss_distinct
21+
, bench "issue1202_repeat" $
22+
whnf (consumeNoFusion . collectFrameworksDirs) xss_repeat
23+
]
24+
, bgroup "nubInt"
25+
[ bench "no_fusion_distinct" $
26+
whnf (consumeNoFusion . LU.nubInt) xs_distinct
27+
, bench "no_fusion_repeat" $
28+
whnf (consumeNoFusion . LU.nubInt) xs_repeat
29+
, bench "issue1202_distinct" $
30+
whnf (consumeNoFusion . collectFrameworksDirs_nubInt) xss_distinct
31+
, bench "issue1202_repeat" $
32+
whnf (consumeNoFusion . collectFrameworksDirs_nubInt) xss_repeat
33+
]
34+
]
35+
where
36+
bound = 1000 :: Int
37+
xs_distinct = [1..bound]
38+
xs_repeat = replicate bound 1 :: [Int]
39+
xss_distinct = [[i] | i <- [1..bound]]
40+
xss_repeat = replicate bound [1] :: [[Int]]
41+
42+
-- Simple version of the case reported in
43+
-- https://github.com/haskell/containers/issues/1202
44+
collectFrameworksDirs :: [[Int]] -> [Int]
45+
collectFrameworksDirs =
46+
map (*2) . LU.nubOrd . filter (/=0) . concatMap id
47+
{-# NOINLINE collectFrameworksDirs #-}
48+
49+
collectFrameworksDirs_nubInt :: [[Int]] -> [Int]
50+
collectFrameworksDirs_nubInt =
51+
map (*2) . LU.nubInt . filter (/=0) . concatMap id
52+
{-# NOINLINE collectFrameworksDirs_nubInt #-}
53+
54+
consumeNoFusion :: [a] -> ()
55+
consumeNoFusion = foldr (\_ z -> z) ()
56+
{-# NOINLINE consumeNoFusion #-}

containers-tests/containers-tests.cabal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ benchmark set-operations-set
286286
build-depends:
287287
benchmark-utils
288288

289+
benchmark listutils
290+
import: benchmark-deps, warnings
291+
default-language: Haskell2010
292+
type: exitcode-stdio-1.0
293+
hs-source-dirs: benchmarks
294+
main-is: ListUtils.hs
295+
ghc-options: -O2
296+
289297
benchmark lookupge-intmap
290298
import: benchmark-deps, warnings
291299
default-language: Haskell2010

containers/src/Data/Containers/ListUtils.hs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import qualified Data.Set as Set
3333
import qualified Data.IntSet as IntSet
3434
import Data.IntSet (IntSet)
3535
#ifdef __GLASGOW_HASKELL__
36-
import GHC.Exts ( build )
36+
import GHC.Exts (build, oneShot)
3737
#endif
3838

3939
-- *** Ord-based nubbing ***
@@ -72,9 +72,8 @@ nubOrd = nubOrdOn id
7272
--
7373
-- @since 0.6.0.1
7474
nubOrdOn :: Ord b => (a -> b) -> [a] -> [a]
75-
-- For some reason we need to write an explicit lambda here to allow this
76-
-- to inline when only applied to a function.
77-
nubOrdOn f = \xs -> nubOrdOnExcluding f Set.empty xs
75+
nubOrdOn f = -- Inline with 1 arg
76+
\xs -> nubOrdOnExcluding f Set.empty xs
7877
{-# INLINE nubOrdOn #-}
7978

8079
-- Splitting nubOrdOn like this means that we don't have to worry about
@@ -110,11 +109,13 @@ nubOrdOnFB :: Ord b
110109
-> (Set b -> r)
111110
-> Set b
112111
-> r
113-
nubOrdOnFB f c x r s
114-
| fx `Set.member` s = r s
115-
| otherwise = x `c` r (Set.insert fx s)
116-
where !fx = f x
117-
{-# INLINABLE [0] nubOrdOnFB #-}
112+
nubOrdOnFB f c = -- Inline with 2 args
113+
\x r -> oneShot (\s ->
114+
let !y = f x
115+
in if y `Set.member` s
116+
then r s
117+
else x `c` r (Set.insert y s))
118+
{-# INLINE [0] nubOrdOnFB #-}
118119

119120
constNubOn :: a -> b -> a
120121
constNubOn x _ = x
@@ -153,9 +154,8 @@ nubInt = nubIntOn id
153154
--
154155
-- @since 0.6.0.1
155156
nubIntOn :: (a -> Int) -> [a] -> [a]
156-
-- For some reason we need to write an explicit lambda here to allow this
157-
-- to inline when only applied to a function.
158-
nubIntOn f = \xs -> nubIntOnExcluding f IntSet.empty xs
157+
nubIntOn f = -- Inline with 1 arg
158+
\xs -> nubIntOnExcluding f IntSet.empty xs
159159
{-# INLINE nubIntOn #-}
160160

161161
-- Splitting nubIntOn like this means that we don't have to worry about
@@ -189,9 +189,11 @@ nubIntOnFB :: (a -> Int)
189189
-> (IntSet -> r)
190190
-> IntSet
191191
-> r
192-
nubIntOnFB f c x r s
193-
| fx `IntSet.member` s = r s
194-
| otherwise = x `c` r (IntSet.insert fx s)
195-
where !fx = f x
196-
{-# INLINABLE [0] nubIntOnFB #-}
192+
nubIntOnFB f c = -- Inline with 2 args
193+
\x r -> oneShot (\s ->
194+
let !y = f x
195+
in if y `IntSet.member` s
196+
then r s
197+
else x `c` r (IntSet.insert y s))
198+
{-# INLINE [0] nubIntOnFB #-}
197199
#endif

0 commit comments

Comments
 (0)