Concurrent Tries with Efficient Non-blocking Snapshots Aleksandar Prokopec Phil Bagwell Martin Odersky École Polytechnique Fédérale de Lausanne Nathan Bronson Stanford
Motivation val numbers = getNumbers() // compute square roots numbers foreach { entry => x = entry.root n = entry.number entry.root = 0.5 * (x + n / x) if (abs(entry.root - x) < eps) numbers.remove(entry) }
Hash Array Mapped Tries (HAMT)
0 =
Hash Array Mapped Tries (HAMT) 0
0 16 =
Hash Array Mapped Tries (HAMT) 016
Hash Array Mapped Tries (HAMT) =
Hash Array Mapped Tries (HAMT) =
Hash Array Mapped Tries (HAMT) 16 04
Hash Array Mapped Tries (HAMT) =
Hash Array Mapped Tries (HAMT) =
Hash Array Mapped Tries (HAMT)
Hash Array Mapped Tries (HAMT)
Hash Array Mapped Tries (HAMT)
Hash Array Mapped Tries (HAMT)
Hash Array Mapped Tries (HAMT)
Hash Array Mapped Tries (HAMT)
Immutable HAMT used as immutable maps in functional languages
Immutable HAMT updates rewrite path from root to leaf insert(11)
Immutable HAMT updates rewrite path from root to leaf insert(11) efficient updates - log k (n)
Node compression BITPOP(((1 > lev) & 1F)) – 1) & BMP)
Node compression
Ctrie Can mutable HAMT be modified to be thread-safe?
Ctrie insert =
Ctrie insert = ) allocate
Ctrie insert = ) CAS
Ctrie insert =
Ctrie insert =
Ctrie insert = ) allocate
Ctrie insert = ) CAS
Ctrie insert = ) CAS Unless…
Ctrie insert = T1-1) allocate Unless… 28 = T1 T2
Ctrie insert = T1-1) allocate Unless… 28 = T1 T T2-1) allocate
Ctrie insert = T1-1) allocate = T1 T T2-2) CAS
Ctrie insert = T1-2) CAS = T1 T T2-2) CAS
Ctrie insert = = T1 T Lost insert!
Ctrie insert – 2 nd attempt Solution: I-nodes
Ctrie insert – 2 nd attempt = = T1 T2
Ctrie insert – 2 nd attempt T1 T = = T2-1) allocate T1-1) allocate
Ctrie insert – 2 nd attempt T1 T T2-2) CAS T1-2) CAS
Ctrie insert – 2 nd attempt
Ctrie insert – 2 nd attempt Idea: once added to the Ctrie, I-nodes remain present.
Ctrie insert – 2 nd attempt Remove operation supported as well - details in the paper.
Ctrie size
Ctrie size size = 0
Ctrie size size = 0
Ctrie size size = 0
Ctrie size size = 0
Ctrie size size = 1
Ctrie size size = 2
Ctrie size size = 3
Ctrie size size = 5
Ctrie size size = 5 actual size = 12
Ctrie size size = 5 01 actual size = 12
Ctrie size size = 5 01 CAS actual size = 11
Ctrie size size = 5 01 actual size = 11
Ctrie size size = 6 01 actual size = 11
Ctrie size size = 6 01 actual size = 11 19
Ctrie size size = 6 01 actual size =
Ctrie size size = 6 01 actual size = CAS
Ctrie size size = 6 01 actual size =
Ctrie size size = 6 01 actual size =
Ctrie size size = 7 01 actual size =
Ctrie size size = 8 01 actual size =
Ctrie size size = 9 01 actual size =
Ctrie size size = actual size =
Ctrie size size = actual size =
Ctrie size size = actual size =
Ctrie size size = actual size =
Ctrie size size = actual size = But the size was never 13!
Global state information size find filter iterator
Global state information size find filter iterator snapshot
Snapshot using locks
Snapshot using locks copy expensive
Snapshot using locks copy expensive not lock-free
Snapshot using locks copy expensive not lock-free can insert or remove remain lock-free? 01 2 CAS
Snapshot using locks copy expensive not lock-free can insert or remove remain lock-free? 01 2 CAS
Snapshot using logs keep a linked list of previous values in each I-node
Snapshot using logs keep a linked list of previous values in each I-node
Snapshot using logs keep a linked list of previous values in each I-node when is it safe to delete old entries? 01 2
Snapshot using immutability root
Snapshot using immutability #1 root
Snapshot using immutability #1 snapshot! root
Snapshot using immutability #1 snapshot! #2 root 1) create new I-node at #2
Snapshot using immutability #1 snapshot! #2 root 2) set snapshot snapshot #1
Snapshot using immutability #1 snapshot! #2 root 3) CAS root to new I-node snapshot #1
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2 generation #2 - ok!
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2 generation #1 not ok, too old!
Snapshot using immutability #1 subsequent insert #2 root 1) create updated node at #2 snapshot #1 2 #2
Snapshot using immutability #1 subsequent insert #2 root 2) CAS to the updated node snapshot #1 2 #2
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2 #2 #1 too old!
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2 # #2 1) create updated node at #2
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 2 # #2 2) CAS
Snapshot using immutability #1 subsequent insert #2 rootsnapshot #1 # # finally, create a new leaf and CAS
Snapshot using immutability #1 another insert #2 rootsnapshot #1 # #
Snapshot using immutability #1 another insert #2 rootsnapshot #1 # #
Snapshot using immutability #1 But... this won't really work... why? #2 rootsnapshot #1 # #
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove CAS
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove CAS How to fail this last CAS?
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove DCAS How to fail this last CAS? DCAS
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove How to fail this last CAS? DCAS - software based DCAS
Snapshot using immutability #1 #2 rootsnapshot #1 # # T2: remove How to fail this last CAS? DCAS - software based...creates intermediate objects DCAS
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # T2: remove prev 1) set prev field
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # T2: remove prev 2) CAS
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # T2: remove prev 3) read root generation
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # prev 4) if root generation changed CAS prev to FailedNode(prev) FN
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # prev 4) if root generation changed CAS prev to FailedNode(prev) FN
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # prev 5) CAS to previous value FN
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # prev 4) if root generation unchanged CAS prev to null
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # ) if root generation unchanged CAS prev to null
GCAS - generation-compare-and-swap #1 #2 rootsnapshot #1 # # ) Replace all CAS with GCAS 2) Replace all READ with GCAS_READ (which checks if prev field is null)
Snapshot-based iterator def iterator = if (isSnapshot) new Iterator(root) else snapshot().iterator()
Snapshot-based size def size = { val sz = 0 val it = iterator while (it.hasNext) sz += 1 sz }
Snapshot-based size def size = { val sz = 0 val it = iterator while (it.hasNext) sz += 1 sz } Above is O(n). But, by caching size in nodes - amortized O(log k n)! (see source code)
Snapshot-based atomic clear def clear() = { val or = READ(root) val nr = new INode(new Gen) if (!CAS(root, or, nr)) clear() } (roughly)
Evaluation - quad core i7
Evaluation – UltraSPARC T2
Evaluation – 4x 8-core i7
Evaluation – snapshot
Conclusion snapshots are linearizable and lock-free snapshots take constant time snapshots are horizontally scalable snapshots add a non-significant overhead to the algorithm if they aren't used the approach may be applicable to tree-based lock-free data-structures in general (intuition)
Thank you!