Skip to content

Commit 9adcd3c

Browse files
authored
Faster List.map (#157)
* List Functor: mix unrolled and reverse map Addresses #131 The relevant chunk sizes (5 for the initial list segment), (3 for the tail-recursive remainder) were arrived at through benchmarked experimentation, mapping a simple (_ + 1) through lists of various sizes. Relevant figures: list of 1000 elems: 142.61 μs -> 36.97 μs list of 2000 elems: 275.17 μs -> 55.33 μs list of 10000 elems: 912.73 μs -> 208.39 μs list of 100000 elems: 34.56 ms -> 1.24 ms The ~30x speed increase for long lists is probably explained by the lack of GC thrashing with this approach. Benchmarked on 2017 Macbook Pro, 2.3 GHz Intel Core i5, 8 GB RAM. macOS Sierra 10.12.6 node v8.9.1 * initial benchmarks for List.map 2017 MacBook Pro 2.3 GHz Intel Core i5, 8 GB 2133 MHz LPDDR3 Node v8.9.1 List ==== map --- map: empty list mean = 1.31 μs stddev = 11.87 μs min = 799.00 ns max = 375.82 μs map: singleton list mean = 2.40 μs stddev = 11.03 μs min = 1.03 μs max = 342.18 μs map: list (1000 elems) mean = 143.41 μs stddev = 225.12 μs min = 97.16 μs max = 2.03 ms map: list (2000 elems) mean = 274.16 μs stddev = 295.84 μs min = 199.66 μs max = 2.06 ms map: list (5000 elems) mean = 531.84 μs stddev = 512.61 μs min = 229.45 μs max = 2.95 ms map: list (10000 elems) mean = 895.24 μs stddev = 777.87 μs min = 464.59 μs max = 2.94 ms map: list (100000 elems) mean = 33.45 ms stddev = 7.65 ms min = 22.07 ms max = 63.47 ms * style tweak * test stack-safety of strict map * lower unrolled map iteration limit this lower the probability of stack-size troubles * restore un-exported functions from Data.List.Types * add failing map test * fix a logic error in List.map chunkedRevMap * make map stack safe(r) again begin with reverse unrolled map * Update for 0.12 id -> identity * Update benchmark code for 0.12 * Remove outdated comment * 🤦‍♂️
1 parent 6629e0c commit 9adcd3c

File tree

6 files changed

+104
-4
lines changed

6 files changed

+104
-4
lines changed

bench/Bench/Data/List.purs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module Bench.Data.List where
2+
3+
import Prelude
4+
import Effect (Effect)
5+
import Effect.Console (log)
6+
import Performance.Minibench (bench)
7+
8+
import Data.List as L
9+
10+
benchList :: Effect Unit
11+
benchList = do
12+
log "map"
13+
log "---"
14+
benchMap
15+
16+
where
17+
18+
benchMap = do
19+
let nats = L.range 0 999999
20+
mapFn = map (_ + 1)
21+
list1000 = L.take 1000 nats
22+
list2000 = L.take 2000 nats
23+
list5000 = L.take 5000 nats
24+
list10000 = L.take 10000 nats
25+
list100000 = L.take 100000 nats
26+
27+
log "map: empty list"
28+
let emptyList = L.Nil
29+
bench \_ -> mapFn emptyList
30+
31+
log "map: singleton list"
32+
let singletonList = L.Cons 0 L.Nil
33+
bench \_ -> mapFn singletonList
34+
35+
log $ "map: list (" <> show (L.length list1000) <> " elems)"
36+
bench \_ -> mapFn list1000
37+
38+
log $ "map: list (" <> show (L.length list2000) <> " elems)"
39+
bench \_ -> mapFn list2000
40+
41+
log $ "map: list (" <> show (L.length list5000) <> " elems)"
42+
bench \_ -> mapFn list5000
43+
44+
log $ "map: list (" <> show (L.length list10000) <> " elems)"
45+
bench \_ -> mapFn list10000
46+
47+
log $ "map: list (" <> show (L.length list100000) <> " elems)"
48+
bench \_ -> mapFn list100000

bench/Main.purs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Bench.Main where
2+
3+
import Prelude
4+
import Effect (Effect)
5+
import Effect.Console (log)
6+
7+
import Bench.Data.List (benchList)
8+
9+
main :: Effect Unit
10+
main = do
11+
log "List"
12+
log "===="
13+
benchList

bower.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"purescript-arrays": "^5.0.0",
3737
"purescript-assert": "^4.0.0",
3838
"purescript-console": "^4.0.0",
39-
"purescript-math": "^2.1.1"
39+
"purescript-math": "^2.1.1",
40+
"purescript-minibench": "^2.0.0"
4041
}
4142
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"scripts": {
44
"clean": "rimraf output && rimraf .pulp-cache",
55
"build": "pulp build -- --censor-lib --strict",
6-
"test": "pulp test --check-main-type Effect.Effect"
6+
"test": "pulp test",
7+
8+
"bench:build": "purs compile 'bench/**/*.purs' 'src/**/*.purs' 'bower_components/*/src/**/*.purs'",
9+
"bench:run": "node --expose-gc -e 'require(\"./output/Bench.Main/index.js\").main()'",
10+
"bench": "npm run bench:build && npm run bench:run"
711
},
812
"devDependencies": {
913
"pulp": "^12.2.0",

src/Data/List/Types.purs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
module Data.List.Types where
1+
module Data.List.Types
2+
( List(..)
3+
, (:)
4+
, NonEmptyList(..)
5+
, toList
6+
, nelCons
7+
) where
28

39
import Prelude
410

@@ -67,7 +73,29 @@ instance monoidList :: Monoid (List a) where
6773
mempty = Nil
6874

6975
instance functorList :: Functor List where
70-
map f = foldr (\x acc -> f x : acc) Nil
76+
map = listMap
77+
78+
-- chunked list Functor inspired by OCaml
79+
-- https://discuss.ocaml.org/t/a-new-list-map-that-is-both-stack-safe-and-fast/865
80+
-- chunk sizes determined through experimentation
81+
listMap :: forall a b. (a -> b) -> List a -> List b
82+
listMap f = chunkedRevMap Nil
83+
where
84+
chunkedRevMap :: List (List a) -> List a -> List b
85+
chunkedRevMap chunksAcc chunk@(x1 : x2 : x3 : xs) =
86+
chunkedRevMap (chunk : chunksAcc) xs
87+
chunkedRevMap chunksAcc xs =
88+
reverseUnrolledMap chunksAcc $ unrolledMap xs
89+
where
90+
unrolledMap :: List a -> List b
91+
unrolledMap (x1 : x2 : Nil) = f x1 : f x2 : Nil
92+
unrolledMap (x1 : Nil) = f x1 : Nil
93+
unrolledMap _ = Nil
94+
95+
reverseUnrolledMap :: List (List a) -> List b -> List b
96+
reverseUnrolledMap ((x1 : x2 : x3 : _) : cs) acc =
97+
reverseUnrolledMap cs (f x1 : f x2 : f x3 : acc)
98+
reverseUnrolledMap _ acc = acc
7199

72100
instance functorWithIndexList :: FunctorWithIndex Int List where
73101
mapWithIndex f = foldrWithIndex (\i x acc -> f i x : acc) Nil

test/Test/Data/List.purs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,12 @@ testList = do
364364
log "map should maintain order"
365365
assert $ (1..5) == map identity (1..5)
366366

367+
log "map should be stack-safe"
368+
void $ pure $ map identity (1..100000)
369+
370+
log "map should be correct"
371+
assert $ (1..1000000) == map (_ + 1) (0..999999)
372+
367373
log "transpose"
368374
assert $ transpose (l [l [1,2,3], l[4,5,6], l [7,8,9]]) ==
369375
(l [l [1,4,7], l[2,5,8], l [3,6,9]])

0 commit comments

Comments
 (0)