Skip to content

Commit bf8f500

Browse files
authored
Merge pull request #27 from tmcgilchrist/ocaml_5_port
Add Michael Scott queue and Treiber stack
2 parents 4f7dfa4 + 08cbcf6 commit bf8f500

21 files changed

+1258
-26
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
A collection of Concurrent Lockfree Data Structures for OCaml 5. It contains:
55

6+
* [Treiber Stack](src/treiber_stack.mli) A classic multi-producer multi-consumer stack, robust and flexible. Recommended starting point when needing LIFO structure.
7+
8+
* [Michael-Scott Queue](src/michael_scott_queue.mli) A classic multi-producer multi-consumer queue, robust and flexible. Recommended starting point when needing FIFO structure. It is based on [Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms](https://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf).
9+
610
* [Chase-Lev Work-Stealing Deque](src/ws_deque.mli) Single-producer, multi-consumer dynamic-size double-ended queue (deque) (see [Dynamic circular work-stealing deque](https://dl.acm.org/doi/10.1145/1073970.1073974) and [Correct and efficient work-stealing for weak memory models](https://dl.acm.org/doi/abs/10.1145/2442516.2442524)). Ideal for throughput-focused scheduling using per-core work distribution. Note, [pop] and [steal] follow different ordering (respectively LIFO and FIFO) and have different linearization contraints.
711

812
* [SPSC Queue](src/spsc_queue.mli) Simple single-producer single-consumer fixed-size queue. Thread-safe as long as at most one thread acts as producer and at most one as consumer at any single point in time.
@@ -11,7 +15,6 @@ A collection of Concurrent Lockfree Data Structures for OCaml 5. It contains:
1115

1216
* [MPSC Queue](src/mpsc_queue.mli) A multi-producer, single-consumer, thread-safe queue without support for cancellation. This makes a good data structure for a scheduler's run queue. It is used in [Eio](https://github.com/ocaml-multicore/eio). It is a single consumer version of the queue described in [Implementing lock-free queues](https://people.cs.pitt.edu/~jacklange/teaching/cs2510-f12/papers/implementing_lock_free.pdf).
1317

14-
1518
## Usage
1619

1720
lockfree cam be installed from `opam`: `opam install lockfree`. Sample usage of

bench/backoff.ml

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
let item_count = 3_000_000
2+
3+
type 'a t = { value : 'a; next : 'a t option Atomic.t }
4+
5+
let empty () = { value = Obj.magic (); next = Atomic.make None }
6+
7+
let push ~backoff_once t value =
8+
let b = Lockfree.Backoff.create () in
9+
let new_head = ({ value; next = Atomic.make None } : 'a t) in
10+
let rec push_f () =
11+
let head = Atomic.get t.next in
12+
Atomic.set new_head.next head;
13+
if Atomic.compare_and_set t.next head (Some new_head) then ()
14+
else (
15+
backoff_once b;
16+
push_f ())
17+
in
18+
push_f ()
19+
20+
let rec pop ?min_wait ~backoff_once t =
21+
let b = Lockfree.Backoff.create ?min_wait () in
22+
let head = Atomic.get t.next in
23+
match head with
24+
| None -> None
25+
| Some node ->
26+
if Atomic.compare_and_set t.next head (Atomic.get node.next) then
27+
Some node.value
28+
else (
29+
backoff_once b;
30+
pop ~backoff_once t)
31+
32+
let run_basic ~backoff_once () =
33+
let stack = empty () in
34+
let pusher =
35+
Domain.spawn (fun () ->
36+
let start_time = Unix.gettimeofday () in
37+
for i = 1 to item_count do
38+
push ~backoff_once stack i
39+
done;
40+
start_time)
41+
in
42+
for _ = 1 to item_count do
43+
while Option.is_none (pop ~backoff_once stack) do
44+
()
45+
done
46+
done;
47+
let end_time = Unix.gettimeofday () in
48+
let start_time = Domain.join pusher in
49+
let time_diff = end_time -. start_time in
50+
time_diff
51+
52+
let run_artificial ~backoff_once () =
53+
let threads = 6 in
54+
let stack = empty () in
55+
56+
(* prepare stack *)
57+
for i = 1 to item_count do
58+
push ~backoff_once stack i
59+
done;
60+
61+
(* *)
62+
let counter = Atomic.make 0 in
63+
let domains =
64+
List.init threads (fun _ ->
65+
Domain.spawn (fun () ->
66+
Atomic.incr counter;
67+
(* wait for all ready *)
68+
while Atomic.get counter <= threads do
69+
()
70+
done;
71+
72+
(* bench !*)
73+
while Option.is_some (pop ~min_wait:100 ~backoff_once stack) do
74+
()
75+
done;
76+
77+
Unix.gettimeofday ()))
78+
in
79+
80+
(* wait for all domains to start *)
81+
while Atomic.get counter < threads do
82+
()
83+
done;
84+
let start_time = Unix.gettimeofday () in
85+
86+
(* let them run! *)
87+
Atomic.incr counter;
88+
89+
(* wait for finish *)
90+
let end_time =
91+
List.map Domain.join domains |> List.fold_left Float.min Float.max_float
92+
in
93+
let time_diff = end_time -. start_time in
94+
time_diff
95+
96+
let bench ~run_type ~with_backoff () =
97+
let backoff_once =
98+
if with_backoff then Lockfree.Backoff.once
99+
else fun (_ : Lockfree.Backoff.t) -> ()
100+
in
101+
let results = ref [] in
102+
let run =
103+
match run_type with `Artificial -> run_artificial | `Basic -> run_basic
104+
in
105+
for i = 1 to 10 do
106+
let time = run ~backoff_once () in
107+
if i > 1 then results := time :: !results
108+
done;
109+
let results = List.sort Float.compare !results in
110+
let median_time = List.nth results 4 in
111+
let median_throughput = Float.of_int item_count /. median_time in
112+
let name =
113+
Printf.sprintf "backoff-%s-%s"
114+
(if with_backoff then "on" else "off")
115+
(match run_type with `Artificial -> "artificial" | `Basic -> "basic")
116+
in
117+
Benchmark_result.create_generic ~median_time ~median_throughput name
118+
119+
let bench_artificial = bench ~run_type:`Artificial
120+
let bench_basic = bench ~run_type:`Basic

bench/backoff.mli

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
val bench_artificial : with_backoff:bool -> unit -> Benchmark_result.t
2+
val bench_basic : with_backoff:bool -> unit -> Benchmark_result.t

bench/bench_spsc_queue.ml

+2-24
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,6 @@ let run () =
2525
let time_diff = end_time -. start_time in
2626
time_diff
2727

28-
let create_output median_time median_throughput =
29-
let time =
30-
({
31-
name = "time";
32-
value = `Numeric median_time;
33-
units = "s";
34-
description = "median time result";
35-
}
36-
: Benchmark_result.Metric.t)
37-
in
38-
let throughput =
39-
({
40-
name = "throughput";
41-
value = `Numeric median_throughput;
42-
units = "item/s";
43-
description = "median throughput result";
44-
}
45-
: Benchmark_result.Metric.t)
46-
in
47-
let metrics = [ time; throughput ] in
48-
({ name = "spsc-queue"; metrics } : Benchmark_result.t)
49-
5028
let bench () =
5129
let results = ref [] in
5230
for i = 1 to 10 do
@@ -55,5 +33,5 @@ let bench () =
5533
done;
5634
let results = List.sort Float.compare !results in
5735
let median_time = List.nth results 4 in
58-
let throughput = Float.of_int item_count /. median_time in
59-
create_output median_time throughput
36+
let median_throughput = Float.of_int item_count /. median_time in
37+
Benchmark_result.create_generic ~median_time ~median_throughput "spsc-queue"

bench/benchmark_result.ml

+28
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,31 @@ type t = { name : string; metrics : Metric.t list }
2222
let to_json { name; metrics } =
2323
let metrics = List.map Metric.to_json metrics |> String.concat ", " in
2424
Printf.sprintf {| {"name": "%s", "metrics": [%s]} |} name metrics
25+
26+
let create_generic ?median_time ?median_throughput name =
27+
let time =
28+
Option.map
29+
(fun median_time : Metric.t ->
30+
{
31+
name = "time";
32+
value = `Numeric median_time;
33+
units = "s";
34+
description = "median time result";
35+
})
36+
median_time
37+
in
38+
let throughput =
39+
Option.map
40+
(fun median_throughput : Metric.t ->
41+
{
42+
name = "throughput";
43+
value = `Numeric median_throughput;
44+
units = "item/s";
45+
description = "median throughput result";
46+
})
47+
median_throughput
48+
in
49+
let metrics = [ time; throughput ] |> List.filter_map (fun v -> v) in
50+
if metrics = [] then
51+
failwith "Benchmark_result.create: require at least one metric";
52+
({ name; metrics } : t)

bench/benchmark_result.mli

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ end
1010
type t = { name : String.t; metrics : Metric.t list }
1111

1212
val to_json : t -> string
13+
14+
val create_generic :
15+
?median_time:float -> ?median_throughput:float -> string -> t

bench/main.ml

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
let backoff_benchmarks =
2+
let open Backoff in
3+
[
4+
bench_basic ~with_backoff:true;
5+
bench_basic ~with_backoff:false;
6+
bench_artificial ~with_backoff:true;
7+
bench_artificial ~with_backoff:false;
8+
]
9+
110
let benchmark_list =
211
[
312
Bench_spsc_queue.bench;
@@ -8,6 +17,7 @@ let benchmark_list =
817
Mpmc_queue.bench ~use_cas:true ~takers:1 ~pushers:8;
918
Mpmc_queue.bench ~use_cas:true ~takers:8 ~pushers:1;
1019
]
20+
@ backoff_benchmarks
1121

1222
let () =
1323
let results =

src/backoff.ml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type t = { min_wait : int; max_wait : int; current : int ref }
2+
3+
let k = Domain.DLS.new_key Random.State.make_self_init
4+
5+
let create ?(min_wait = 17) ?(max_wait = 32 * 4096) () =
6+
{ max_wait; min_wait; current = ref min_wait }
7+
8+
let once { max_wait; current; _ } =
9+
let t = Random.State.int (Domain.DLS.get k) !current in
10+
current := min (2 * !current) max_wait;
11+
if t = 0 then ()
12+
else
13+
for _ = 1 to t do
14+
Domain.cpu_relax ()
15+
done
16+
17+
let reset { min_wait; current; _ } = current := min_wait

src/backoff.mli

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
(** Truncated exponential backoff.
2+
3+
Generally, a backoff mechanism adjusts time waited between retries to ensure the retries
4+
will not put too much pressure on some underlying system. This particular implementation
5+
is built for reducing contention in lockfree algorithms.
6+
7+
Under the hood, it uses relevant pause instruction to avoid adverse
8+
microarchitectural effects present in naive busy-looping.
9+
10+
*)
11+
12+
type t
13+
(** [t] type of a backoff object. *)
14+
15+
val create : ?min_wait:int -> ?max_wait:int -> unit -> t
16+
(** [create] creates a new instance of backoff. [max_wait], [min_wait] override
17+
the upper and lower bound on the number of spins executed by [once]. *)
18+
19+
val once : t -> unit
20+
(** [once] executes one wait, whose length increases for every consecutive attempt
21+
(until [max] is reached). *)
22+
23+
val reset : t -> unit
24+
(** [reset] resets the attempt counter in [t]. *)

src/lockfree.ml

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
Copyright (c) 2017, Nicolas ASSOUAD <[email protected]>
2626
########
2727
*)
28-
2928
module Ws_deque = Ws_deque
3029
module Spsc_queue = Spsc_queue
3130
module Mpsc_queue = Mpsc_queue
31+
module Treiber_stack = Treiber_stack
32+
module Michael_scott_queue = Michael_scott_queue
33+
module Backoff = Backoff
3234
module Mpmc_relaxed_queue = Mpmc_relaxed_queue

src/lockfree.mli

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ Copyright (c) 2017, Nicolas ASSOUAD <[email protected]>
3333
module Ws_deque = Ws_deque
3434
module Spsc_queue = Spsc_queue
3535
module Mpsc_queue = Mpsc_queue
36+
module Treiber_stack = Treiber_stack
37+
module Michael_scott_queue = Michael_scott_queue
3638
module Mpmc_relaxed_queue = Mpmc_relaxed_queue
39+
module Backoff = Backoff

src/michael_scott_queue.ml

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
(*
2+
* Copyright (c) 2015, Théo Laurent <[email protected]>
3+
* Copyright (c) 2015, KC Sivaramakrishnan <[email protected]>
4+
*
5+
* Permission to use, copy, modify, and/or distribute this software for any
6+
* purpose with or without fee is hereby granted, provided that the above
7+
* copyright notice and this permission notice appear in all copies.
8+
*
9+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16+
*)
17+
18+
(* Michael-Scott queue *)
19+
20+
type 'a node = Nil | Next of 'a * 'a node Atomic.t
21+
type 'a t = { head : 'a node Atomic.t; tail : 'a node Atomic.t }
22+
23+
let create () =
24+
let head = Next (Obj.magic (), Atomic.make Nil) in
25+
{ head = Atomic.make head; tail = Atomic.make head }
26+
27+
let is_empty q =
28+
match Atomic.get q.head with
29+
| Nil -> failwith "MSQueue.is_empty: impossible"
30+
| Next (_, x) -> ( match Atomic.get x with Nil -> true | _ -> false)
31+
32+
let pop q =
33+
let b = Backoff.create () in
34+
let rec loop () =
35+
let s = Atomic.get q.head in
36+
let nhead =
37+
match s with
38+
| Nil -> failwith "MSQueue.pop: impossible"
39+
| Next (_, x) -> Atomic.get x
40+
in
41+
match nhead with
42+
| Nil -> None
43+
| Next (v, _) when Atomic.compare_and_set q.head s nhead -> Some v
44+
| _ ->
45+
Backoff.once b;
46+
loop ()
47+
in
48+
loop ()
49+
50+
let push q v =
51+
let rec find_tail_and_enq curr_end node =
52+
if Atomic.compare_and_set curr_end Nil node then ()
53+
else
54+
match Atomic.get curr_end with
55+
| Nil -> find_tail_and_enq curr_end node
56+
| Next (_, n) -> find_tail_and_enq n node
57+
in
58+
let newnode = Next (v, Atomic.make Nil) in
59+
let tail = Atomic.get q.tail in
60+
match tail with
61+
| Nil -> failwith "HW_MSQueue.push: impossible"
62+
| Next (_, n) ->
63+
find_tail_and_enq n newnode;
64+
ignore (Atomic.compare_and_set q.tail tail newnode)
65+
66+
let clean_until q f =
67+
let b = Backoff.create () in
68+
let rec loop () =
69+
let s = Atomic.get q.head in
70+
let nhead =
71+
match s with
72+
| Nil -> failwith "MSQueue.pop: impossible"
73+
| Next (_, x) -> Atomic.get x
74+
in
75+
match nhead with
76+
| Nil -> ()
77+
| Next (v, _) ->
78+
if not (f v) then
79+
if Atomic.compare_and_set q.head s nhead then (
80+
Backoff.reset b;
81+
loop ())
82+
else (
83+
Backoff.once b;
84+
loop ())
85+
else ()
86+
in
87+
loop ()
88+
89+
type 'a cursor = 'a node
90+
91+
let snapshot q =
92+
match Atomic.get q.head with
93+
| Nil -> failwith "MSQueue.snapshot: impossible"
94+
| Next (_, n) -> Atomic.get n
95+
96+
let next c = match c with Nil -> None | Next (a, n) -> Some (a, Atomic.get n)

0 commit comments

Comments
 (0)