Skip to content

Commit 4f9fc0f

Browse files
committed
Add Dllist.move_l node list and Dllist.move_r node list
1 parent 7aa7e70 commit 4f9fc0f

File tree

4 files changed

+197
-7
lines changed

4 files changed

+197
-7
lines changed

README.md

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ is distributed under the [ISC license](LICENSE.md).
5353
- [Composing transactions](#composing-transactions)
5454
- [Blocking transactions](#blocking-transactions)
5555
- [A transactional lock-free leftist heap](#a-transactional-lock-free-leftist-heap)
56+
- [Programming with transactional data structures](#programming-with-transactional-data-structures)
57+
- [A transactional LRU cache](#a-transactional-lru-cache)
5658
- [Programming with primitive operations](#programming-with-primitive-operations)
5759
- [Designing lock-free algorithms with k-CAS](#designing-lock-free-algorithms-with-k-cas)
5860
- [Minimize accesses](#minimize-accesses)
@@ -713,6 +715,142 @@ Notice how we were able to use a `while` loop, rather than recursion, in
713715
> lock-free heap implementation, but it was pretty straightforward to implement
714716
> using k-CAS based on a textbook imperative implementation.
715717
718+
### Programming with transactional data structures
719+
720+
When was the last time you implemented a non-trivial data structure or algorithm
721+
from scratch? For most professionals the answer might be along the lines of
722+
_"when I took my data structures course at the university"_ or _"when I
723+
interviewed for the software engineering position at Big Co"_.
724+
725+
**kcas** aims to be usable both
726+
727+
- for experts implementing correct and performant lock-free data structures, and
728+
- for everyone gluing together programs using such data structures.
729+
730+
Many of the examples in this introduction are data structures of some sort.
731+
However, implementing basic data structures from scratch is not something
732+
everyone should be doing every time they are writing concurrent programs.
733+
Instead programmers should be able to reuse carefully constructed data
734+
structures.
735+
736+
One source of ready-made data structures is the
737+
[**kcas_data**](https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/index.html)
738+
package. Let's explore how we can leverage those data structures. Of course,
739+
first we need to `#require` the package and we'll also open it for convenience:
740+
741+
```ocaml
742+
# #require "kcas_data"
743+
# open Kcas_data
744+
```
745+
746+
#### A transactional LRU cache
747+
748+
A LRU or least-recently-used cache is essentially a bounded association table.
749+
When the capacity of the cache is exceeded, some association is dropped. The LRU
750+
or least-recently-used policy is to drop the association that was accessed least
751+
recently.
752+
753+
A simple way to implement a LRU cache is to use a hash table to store the
754+
associations and a doubly-linked list to keep track of the order in which
755+
associations have been accessed. Whenever an association is accessed, the
756+
corresponding linked list node is added or moved to one end of the list. When
757+
the cache overflows, the association whose node is at the other end the list is
758+
removed.
759+
760+
The **kcas_data** package conveniently provides a
761+
[`Hashtbl`](https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Hashtbl/index.html)
762+
module providing a hash table implementation that mimics the Stdlib
763+
[`Hashtbl`](https://v2.ocaml.org/api/Hashtbl.html) module and a
764+
[`Dllist`](https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Dllist/index.html)
765+
providing a doubly-linked list implementation. We'll also keep track of the
766+
space in the cache using a separate shared memory location so that it is
767+
possible to change the capacity of the cache dynamically:
768+
769+
```ocaml
770+
type ('k, 'v) cache = {
771+
space: int Loc.t;
772+
table: ('k, 'k Dllist.node * 'v) Hashtbl.t;
773+
order: 'k Dllist.t;
774+
}
775+
```
776+
777+
To create a cache we just create the data structures:
778+
779+
```ocaml
780+
# let cache ?hashed_type capacity =
781+
{ space = Loc.make capacity;
782+
table = Hashtbl.create ?hashed_type ();
783+
order = Dllist.create () }
784+
val cache : ?hashed_type:'a Hashtbl.hashed_type -> int -> ('a, 'b) cache =
785+
<fun>
786+
```
787+
788+
Note that above we just passed the optional `hashed_type` argument to the hash
789+
table constructor. The hash table
790+
[`create`](https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Hashtbl/index.html#val-create)
791+
function takes some more optional arguments some of which might make sense to
792+
pass through.
793+
794+
To access an association in the cache we provide a `get_opt` operation
795+
796+
```ocaml
797+
# let get_opt ~xt {table; order; _} key =
798+
Hashtbl.Xt.find_opt ~xt table key
799+
|> Option.map @@ fun (node, value) ->
800+
Dllist.Xt.move_l ~xt node order;
801+
value
802+
val get_opt : xt:'a Xt.t -> ('b, 'c) cache -> 'b -> 'c option = <fun>
803+
```
804+
805+
that, as explained previously, moves the node corresponding to the accessed
806+
association to the left end of the list.
807+
808+
To introduce associations we provide the `set` operation
809+
810+
```ocaml
811+
# let set ~xt {table; order; space; _} key value =
812+
let node =
813+
match Hashtbl.Xt.find_opt ~xt table key with
814+
| None ->
815+
if 0 = Xt.update ~xt space (fun n -> Int.max 0 (n-1)) then
816+
Dllist.Xt.take_opt_r ~xt order
817+
|> Option.iter (Hashtbl.Xt.remove ~xt table);
818+
Dllist.Xt.add_l ~xt key order
819+
| Some (node, _) ->
820+
Dllist.Xt.move_l ~xt node order;
821+
node
822+
in
823+
Hashtbl.Xt.replace ~xt table key (node, value)
824+
val set : xt:'a Xt.t -> ('b, 'c) cache -> 'b -> 'c -> unit = <fun>
825+
```
826+
827+
that, like `get_opt`, either moves or adds the node of the accessed association
828+
to the left end of the list. In case a new association is added, the space is
829+
decremented. If there was no space, an association is first removed. As
830+
described previously, the association to remove is determined by removing the
831+
rightmost element from the list.
832+
833+
We can then test that the cache works as expected:
834+
835+
```ocaml
836+
# let a_cache : (int, string) cache = cache 2
837+
val a_cache : (int, string) cache =
838+
{space = <abstr>; table = <abstr>; order = <abstr>}
839+
# Xt.commit { tx = set a_cache 101 "basics" }
840+
- : unit = ()
841+
# Xt.commit { tx = set a_cache 42 "answer" }
842+
- : unit = ()
843+
# Xt.commit { tx = get_opt a_cache 101 }
844+
- : string option = Some "basics"
845+
# Xt.commit { tx = set a_cache 2023 "year" }
846+
- : unit = ()
847+
# Xt.commit { tx = get_opt a_cache 42 }
848+
- : string option = None
849+
```
850+
851+
As an exercise, implement an operation to `remove` associations from a cache and
852+
an operation to change the capacity of the cache.
853+
716854
### Programming with primitive operations
717855

718856
The [`Op`](https://ocaml-multicore.github.io/kcas/doc/kcas/Kcas/Op/index.html)
@@ -1392,7 +1530,7 @@ accessing the association list location corresponding to specified key:
13921530
# let access ~xt basic_hashtbl key =
13931531
let data = Xt.get ~xt basic_hashtbl.data in
13941532
let n = Array.length data in
1395-
let i = Hashtbl.hash key mod n in
1533+
let i = Stdlib.Hashtbl.hash key mod n in
13961534
data.(i)
13971535
val access :
13981536
xt:'a Xt.t -> ('b, 'c) basic_hashtbl -> 'd -> ('b * 'c Loc.t) list Loc.t =
@@ -1457,7 +1595,7 @@ operation:
14571595
|> Array.iter @@ fun assoc_loc ->
14581596
Xt.get ~xt assoc_loc
14591597
|> List.iter @@ fun ((key, _) as bucket) ->
1460-
let i = Hashtbl.hash key mod new_capacity in
1598+
let i = Stdlib.Hashtbl.hash key mod new_capacity in
14611599
Xt.modify ~xt new_data.(i) (List.cons bucket)
14621600
val rehash : xt:'a Xt.t -> ('b, 'c) basic_hashtbl -> int -> unit = <fun>
14631601
```
@@ -1743,11 +1881,6 @@ implementations that are conveniently provided by the
17431881
[**kcas_data**](https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/index.html)
17441882
package.
17451883

1746-
```ocaml
1747-
# #require "kcas_data"
1748-
# open Kcas_data
1749-
```
1750-
17511884
Here is the full toy scheduler module:
17521885

17531886
```ocaml

src/kcas_data/dllist.ml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ module Xt = struct
5959
Xt.set ~xt prev.next (as_list node);
6060
node
6161

62+
let move_l ~xt node list =
63+
let node = as_list node in
64+
let list_next = Xt.exchange ~xt list.next node in
65+
if list_next != node then (
66+
let node_prev = Xt.exchange ~xt node.prev list in
67+
let node_next = Xt.exchange ~xt node.next list_next in
68+
if node_prev != node then (
69+
Xt.set ~xt node_prev.next node_next;
70+
Xt.set ~xt node_next.prev node_prev);
71+
Xt.set ~xt list_next.prev node)
72+
73+
let move_r ~xt node list =
74+
let node = as_list node in
75+
let list_prev = Xt.exchange ~xt list.prev node in
76+
if list_prev != node then (
77+
let node_next = Xt.exchange ~xt node.next list in
78+
let node_prev = Xt.exchange ~xt node.prev list_prev in
79+
if node_next != node then (
80+
Xt.set ~xt node_prev.next node_next;
81+
Xt.set ~xt node_next.prev node_prev);
82+
Xt.set ~xt list_prev.next node)
83+
6284
let take_opt_l ~xt list =
6385
let next = Xt.get ~xt list.next in
6486
if next == list then None
@@ -140,6 +162,8 @@ let add_r value list =
140162
let node = create_node ~prev:list ~next:list value in
141163
Kcas.Xt.commit { tx = Xt.add_node_r node list }
142164

165+
let move_l node list = Kcas.Xt.commit { tx = Xt.move_l node list }
166+
let move_r node list = Kcas.Xt.commit { tx = Xt.move_r node list }
143167
let take_opt_l list = Kcas.Xt.commit { tx = Xt.take_opt_l list }
144168
let take_opt_r list = Kcas.Xt.commit { tx = Xt.take_opt_r list }
145169
let take_blocking_l list = Kcas.Xt.commit { tx = Xt.take_blocking_l list }

src/kcas_data/dllist_intf.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ module type Ops = sig
77
(** [remove n] removes the node [n] from the doubly-linked list it is part of.
88
[remove] is idempotent. *)
99

10+
val move_l : ('x, 'a node -> 'a t -> unit) fn
11+
(** [move_to_l n l] removes the node [n] from the doubly linked list it is
12+
part of and then add it to the left of list [l]. *)
13+
14+
val move_r : ('x, 'a node -> 'a t -> unit) fn
15+
(** [move_to_r n l] removes the node [n] from the doubly linked list it is
16+
part of and then add it to the right of list [l]. *)
17+
1018
val is_empty : ('x, 'a t -> bool) fn
1119
(** [is_empty l] determines whether the doubly-linked list [l] is empty or
1220
not. *)

test/kcas_data/dllist_test.ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,28 @@ let () =
3838
Dllist.add_l 3 l |> ignore;
3939
Dllist.add_r 4 l |> ignore;
4040
assert (take_as_list Dllist.take_opt_l l = [ 3; 1; 4 ])
41+
42+
let () =
43+
let t1 = Dllist.create () in
44+
let n1 = Dllist.add_l 5.3 t1 in
45+
Dllist.move_l n1 t1;
46+
assert (Dllist.to_list_l t1 = [ 5.3 ]);
47+
Dllist.move_r n1 t1;
48+
assert (Dllist.to_list_l t1 = [ 5.3 ]);
49+
let n2 = Dllist.add_l 5.2 t1 in
50+
assert (Dllist.to_list_l t1 = [ 5.2; 5.3 ]);
51+
Dllist.move_r n2 t1;
52+
assert (Dllist.to_list_l t1 = [ 5.3; 5.2 ]);
53+
Dllist.move_l n2 t1;
54+
assert (Dllist.to_list_l t1 = [ 5.2; 5.3 ]);
55+
let t2 = Dllist.create () in
56+
Dllist.move_l n1 t2;
57+
assert (Dllist.to_list_l t1 = [ 5.2 ]);
58+
assert (Dllist.to_list_l t2 = [ 5.3 ]);
59+
Dllist.move_r n2 t2;
60+
assert (Dllist.to_list_l t2 = [ 5.3; 5.2 ]);
61+
Dllist.move_l n1 t1;
62+
assert (Dllist.to_list_l t2 = [ 5.2 ]);
63+
assert (Dllist.to_list_l t1 = [ 5.3 ])
64+
65+
let () = Printf.printf "Test Dllist OK!\n%!"

0 commit comments

Comments
 (0)