Megarefs
And other associative ref types hacks
@cgrand
http://lambdanext.eu
Slide 2
Slide 2 text
I have a problem
• I can’t decide how to split the application
essential state in discrete «boxes»
• Changing the state split later on is not
painless.
• I just want to put the world in a single box!
Slide 3
Slide 3 text
A world in an atom
Slide 4
Slide 4 text
Hail the uniform update
model!
(def a (atom some-big-and-deep-map))
(swap! a update-in path f x y z)
; concatenative feeling...
Slide 5
Slide 5 text
swap-in! v0.0.1
(defn swap-in! [a path f & args]
(apply swap! a
update-in path f args))
; before
(swap! a update-in path f x y z)
; after
(swap-in! a path f x y z)
Slide 6
Slide 6 text
Problems
• Unrelated calls to swap-in! conflict
• Everything (update-in and f) is retried
• When unrelated only update-in should
retry
• f should not retry
Slide 7
Slide 7 text
swap-in! v0.0.2
; memoize makes retries cheap!
(defn swap-in! [a path f & args]
(apply swap! a
update-in path (memoize f) args))
Slide 8
Slide 8 text
swap-in! v0.0.3+
; from here only optimization is better
; cheaper caching: 1-slot, identity,
; update-in steps, etc.
(defn swap-in! [a path f & args]
(swap! a update-in path
(memoize #(apply f % args))))
Slide 9
Slide 9 text
MegaAtom?
• swap-in! allows:
• bigger values
• longer updates
• Still a regular Atom
• Let’s take the next step: multiple paths!
Slide 10
Slide 10 text
Megalomania!
;; update many paths
(defn update-ins [m f & paths]
(reduce-kv assoc-in
m (zipmap paths
(apply f (map #(get-in m %)
paths)))))
(defn swap-ins! [a f & paths]
(apply swap! a
update-ins (memoize f) paths))
Slide 11
Slide 11 text
(def accounts
(atom {:lucy 100 :ethel 200
:fred 300 :ricky 400}))
(defn transfer [amount]
(fn [from to]
[(- from amount) (+ to amount)]))
;; No retry of the transfer fn!
(swap-ins! accounts (transfer 50)
[:ricky] [:lucy])
(swap-ins! accounts (transfer 20)
[:ethel] [:fred])
Slide 12
Slide 12 text
Megalomania!!!
• swap-ins! is a poor man’s STM
• paths as an alternative to discrete identities
• intriguing idea
• let’s try on the STM proper!
Slide 13
Slide 13 text
A world in a ref
Slide 14
Slide 14 text
Refs
• The memoize trick doesn’t work
• There’s no single pure function for the
whole transaction
• Take a page from j.u.concurrent’s book:
• Lock striping
Slide 15
Slide 15 text
Lock striping
• Variable-sized mutable collection
• Fixed number (N) of guards (locks)
• To have exclusive access to the item ith
• lock the (i mod N)th guard!
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2
Slide 16
Slide 16 text
Ref striping
• Variable-sized immutable collection
• Stored in a single root ref
• Fixed number (N) of guards (refs)
• To perform actions (ensure, ref-set/alter,
commute) on the item ith
• perform dummy actions on the (i mod
N)th guard!
• deref or commute the root ref!
Slide 17
Slide 17 text
Why not locks?
• Locks don’t compose
• Can’t do in user code
• Hacking the STM itself
• I failed/abandoned
• Key insight: refs as the building blocks of
more complex ref types.
Slide 18
Slide 18 text
naive alter-nth
(defn alter-nth [r i f & args]
(apply alter r update-in [i] f args))
ref-striped alter-nth
(defn alter-nth [r i f & args]
(let [guards (-> r meta ::guards)
guard (nth guards
(mod i (count guards)))]
(ref-set guard nil)
(apply commute r
update-in [i] f args)))
From -nth to -in
• Maps? (and vectors, records...)
• Just (mod (hash k) (count guards))
• Nested maps?
• Glad you asked!
Slide 23
Slide 23 text
Paths
• You want independent updates for:
• [:a :b] and [:a :c]
• You don’t want independent updates for:
• [:a] and [:a :b]
Slide 24
Slide 24 text
Guards! Guards!
• One guard by prefix (or path segment*)
• (guard [:a]), (guard [:a :b]) etc.
• prefix guards are ensured
• path guard is ref-set
*tends to generate more collisions
Semantics
deref-in/ensure-in/commute-in/alter-in/ref-
set-in
have the same semantics as
deref/ensure/commute/alter/ref-set
combined with get-in/update-in/assoc-in
but they allow for more
concurrency
Slide 28
Slide 28 text
Compatibility
• megaref provides drop-in replacements for
ensure/commute/alter/ref-set
• -in variants works both on refs and
megarefs
• subrefs
Slide 29
Slide 29 text
Subrefs
• A mutable view on a part of a bigger ref
• (subref parent-ref path)
• Allow for easy migration of a codebase
• from plenty-of-refs to some-megarefs
Ants: ported (2/3)
-;world is a 2d vector of refs to cells
-(def world
- (apply vector
- (map (fn [_]
- (apply vector (map (fn [_] (ref (struct
cell 0 0)))
- (range dim))))
- (range dim))))
+;world is a megaref to a 2d vector of cells
+(def world
+ (megaref (vec (repeat dim (vec (repeat dim (struct cell 0
0)))))))
-(defn place [[x y]]
- (-> world (nth x) (nth y)))
+(defn place [xy]
+ (subref world xy))
Summary
• Megaref
• No upfront decision
• Comparable to plenty-of-refs (when not
CPU bound)
• Cheap snapshot without memory
footprint
Slide 36
Slide 36 text
Further ideas
• Allocation profiling
• Tuning the path hash function
• more guards for hot paths
• self-tuning?
• Applying the same principle to agents
Slide 37
Slide 37 text
A world in an agent
Caution: wet paint!
Slide 38
Slide 38 text
MegaAgents
• Relax the serial constraint:
• two actions are serialized only when
their paths are related
• [:a :b] blocks [:a :b :c] (and vice-versa)
• [:a :b] and [:a :c] and [:c :d] occurs
concurrently
Slide 39
Slide 39 text
Agent striping
• Variable-sized immutable collection
• Stored in a single root atom
• Fixed number (N) of guards (agents)
• To perform send f on the item ith
• send
(fn [_] (swap! root assoc-in [i]
(f (get-in path @root))))
to the (i mod N)th guard!
Slide 40
Slide 40 text
From -nth to -in
• Breakthrough by Meikel Brandmeyer:
• chain sends through guards
• Use guards’ values to hold kind of a
counter and a queue
• It even seems to work!
Slide 41
Slide 41 text
Agent Guards!
:a :b :c
:d
:e :f
Not busy
Busy
[2 []] [2 []] [2 [X]]
[0 []]
[1 []] [0 []]
X Pending update to [:a :b :c]
Slide 42
Slide 42 text
Thanks!
https://github.com/cgrand/megaref
(MegaAgents not there yet)
@cgrand
http://lambdanext.eu