Skip to content

Commit 40c5ee2

Browse files
committed
Rewrite or refactor benchmarks using multicore-bench
1 parent 65211c5 commit 40c5ee2

23 files changed

+304
-468
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ clean:
1010
dune clean
1111

1212
bench:
13-
@dune exec --release -- ./bench/main.exe
13+
@dune exec --release -- ./bench/main.exe -budget 1

bench/README.md

+1-7
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,4 @@ Benchmarks for Saturn
44

55
Execute `make bench` from root of the repository to run the standard set of
66
benchmarks. The output is in JSON, as it is intended to be consumed by
7-
ocaml-benchmark CI (in progress).
8-
9-
# Specific structures
10-
11-
Some benchmarks expose commandline interface targeting particular structures:
12-
13-
- [mpmc_queue.exe](mpmc_queue_cmd.ml)
7+
[current-bench](https://bench.ci.dev/ocaml-multicore/saturn/branch/main/benchmark/default).

bench/bench_queue.ml

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
open Multicore_bench
2+
module Queue = Saturn_lockfree.Queue
3+
4+
let run_one ~budgetf ?(n_adders = 2) ?(n_takers = 2)
5+
?(n_msgs = 50 * Util.iter_factor) () =
6+
let n_domains = n_adders + n_takers in
7+
8+
let t = Queue.create () in
9+
10+
let n_msgs_to_take = Atomic.make 0 |> Multicore_magic.copy_as_padded in
11+
let n_msgs_to_add = Atomic.make 0 |> Multicore_magic.copy_as_padded in
12+
13+
let init _ =
14+
Atomic.set n_msgs_to_take n_msgs;
15+
Atomic.set n_msgs_to_add n_msgs
16+
in
17+
let work i () =
18+
if i < n_adders then
19+
let rec work () =
20+
let n = Util.alloc n_msgs_to_add in
21+
if 0 < n then begin
22+
for i = 1 to n do
23+
Queue.push t i
24+
done;
25+
work ()
26+
end
27+
in
28+
work ()
29+
else
30+
let rec work () =
31+
let n = Util.alloc n_msgs_to_take in
32+
if n <> 0 then
33+
let rec loop n =
34+
if 0 < n then
35+
loop (n - Bool.to_int (Option.is_some (Queue.pop_opt t)))
36+
else work ()
37+
in
38+
loop n
39+
in
40+
work ()
41+
in
42+
43+
let config =
44+
let format role n =
45+
Printf.sprintf "%d %s%s" n role (if n = 1 then "" else "s")
46+
in
47+
Printf.sprintf "%s, %s"
48+
(format "nb adder" n_adders)
49+
(format "nb taker" n_takers)
50+
in
51+
52+
Times.record ~budgetf ~n_domains ~init ~work ()
53+
|> Util.thruput_metrics ~n:n_msgs ~singular:"message" ~config
54+
55+
let run_suite ~budgetf =
56+
Util.cross [ 1; 2 ] [ 1; 2 ]
57+
|> List.concat_map @@ fun (n_adders, n_takers) ->
58+
run_one ~budgetf ~n_adders ~n_takers ()

bench/bench_relaxed_queue.ml

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
open Multicore_bench
2+
module Queue = Saturn.Relaxed_queue
3+
module Spin = Queue.Spin
4+
module Not_lockfree = Queue.Not_lockfree
5+
module CAS_interface = Queue.Not_lockfree.CAS_interface
6+
7+
let run_one ~budgetf ~n_adders ~n_takers ?(n_msgs = 50 * Util.iter_factor)
8+
?(api = `Spin) () =
9+
let n_domains = n_adders + n_takers in
10+
11+
let t = Queue.create ~size_exponent:10 () in
12+
13+
let n_msgs_to_take = Atomic.make 0 |> Multicore_magic.copy_as_padded in
14+
let n_msgs_to_add = Atomic.make 0 |> Multicore_magic.copy_as_padded in
15+
16+
let init _ =
17+
Atomic.set n_msgs_to_take n_msgs;
18+
Atomic.set n_msgs_to_add n_msgs
19+
in
20+
let work i () =
21+
if i < n_adders then
22+
let rec work () =
23+
let n = Util.alloc n_msgs_to_add in
24+
if n <> 0 then begin
25+
match api with
26+
| `Spin ->
27+
for i = 1 to n do
28+
Spin.push t i
29+
done;
30+
work ()
31+
| `Not_lockfree ->
32+
let rec loop n =
33+
if 0 < n then loop (n - Bool.to_int (Not_lockfree.push t i))
34+
else work ()
35+
in
36+
loop n
37+
| `CAS_interface ->
38+
let rec loop n =
39+
if 0 < n then loop (n - Bool.to_int (CAS_interface.push t i))
40+
else work ()
41+
in
42+
loop n
43+
end
44+
in
45+
work ()
46+
else
47+
let rec work () =
48+
let n = Util.alloc n_msgs_to_take in
49+
if n <> 0 then
50+
match api with
51+
| `Spin ->
52+
for _ = 1 to n do
53+
Spin.pop t |> ignore
54+
done;
55+
work ()
56+
| `Not_lockfree ->
57+
let rec loop n =
58+
if 0 < n then
59+
loop (n - Bool.to_int (Option.is_some (Not_lockfree.pop t)))
60+
else work ()
61+
in
62+
loop n
63+
| `CAS_interface ->
64+
let rec loop n =
65+
if 0 < n then
66+
loop (n - Bool.to_int (Option.is_some (CAS_interface.pop t)))
67+
else work ()
68+
in
69+
loop n
70+
in
71+
work ()
72+
in
73+
74+
let config =
75+
let format role n =
76+
Printf.sprintf "%d %s%s" n role (if n = 1 then "" else "s")
77+
in
78+
Printf.sprintf "%s, %s (%s)" (format "adder" n_adders)
79+
(format "taker" n_takers)
80+
(match api with
81+
| `Spin -> "spin"
82+
| `Not_lockfree -> "not lf"
83+
| `CAS_interface -> "cas")
84+
in
85+
86+
Times.record ~budgetf ~n_domains ~init ~work ()
87+
|> Util.thruput_metrics ~n:n_msgs ~singular:"message" ~config
88+
89+
let run_suite ~budgetf =
90+
Util.cross
91+
[ `Spin; `Not_lockfree; `CAS_interface ]
92+
(Util.cross [ 1; 2 ] [ 1; 2 ])
93+
|> List.concat_map @@ fun (api, (n_adders, n_takers)) ->
94+
run_one ~budgetf ~n_adders ~n_takers ~api ()

bench/bench_size.ml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
open Multicore_bench
2+
module Size = Saturn_lockfree.Size
3+
4+
let run_one ~budgetf ~n_domains ?(n_ops = 250 * n_domains * Util.iter_factor) ()
5+
=
6+
let t = Size.create () in
7+
8+
let n_ops_todo = Atomic.make 0 |> Multicore_magic.copy_as_padded in
9+
10+
let init _ = Atomic.set n_ops_todo n_ops in
11+
let work _ () =
12+
let rec work () =
13+
let n = Util.alloc n_ops_todo in
14+
if n <> 0 then
15+
let rec loop n =
16+
if 0 < n then begin
17+
let incr = Size.new_once t Size.incr in
18+
Size.update_once t incr;
19+
let decr = Size.new_once t Size.decr in
20+
Size.update_once t decr;
21+
loop (n - 2)
22+
end
23+
else work ()
24+
in
25+
loop n
26+
in
27+
work ()
28+
in
29+
30+
let config =
31+
Printf.sprintf "%d worker%s" n_domains (if n_domains = 1 then "" else "s")
32+
in
33+
Times.record ~budgetf ~n_domains ~init ~work ()
34+
|> Util.thruput_metrics ~n:n_ops ~config ~singular:"operation"
35+
36+
let run_suite ~budgetf =
37+
[ 1; 2; 4 ]
38+
|> List.concat_map @@ fun n_domains -> run_one ~n_domains ~budgetf ()

bench/bench_skiplist.ml

+67-54
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,70 @@
1-
open Saturn
2-
3-
let workload num_elems num_threads add remove =
4-
let sl = Skiplist.create ~compare:Int.compare () in
5-
let elems = Array.init num_elems (fun _ -> Random.int 10000) in
6-
let push () =
7-
Domain.spawn (fun () ->
8-
let start_time = Unix.gettimeofday () in
9-
for i = 0 to (num_elems - 1) / num_threads do
10-
Domain.cpu_relax ();
11-
let prob = Random.float 1.0 in
12-
if prob < add then Skiplist.try_add sl (Random.int 10000) () |> ignore
13-
else if prob >= add && prob < add +. remove then
14-
Skiplist.try_remove sl (Random.int 10000) |> ignore
15-
else Skiplist.mem sl elems.(i) |> ignore
16-
done;
17-
start_time)
1+
open Multicore_bench
2+
module Skiplist = Saturn.Skiplist
3+
4+
let run_one ~budgetf ~n_domains ?(n_ops = 20 * Util.iter_factor)
5+
?(n_keys = 10000) ~percent_mem ?(percent_add = (100 - percent_mem + 1) / 2)
6+
?(prepopulate = true) () =
7+
let percent_rem = 100 - (percent_mem + percent_add) in
8+
9+
let limit_mem = percent_mem in
10+
let limit_add = percent_mem + percent_add in
11+
12+
assert (0 <= limit_mem && limit_mem <= 100);
13+
assert (limit_mem <= limit_add && limit_add <= 100);
14+
15+
let t = Skiplist.create ~compare:Int.compare () in
16+
if prepopulate then
17+
for _ = 1 to n_keys do
18+
let value = Random.bits () in
19+
let key = value mod n_keys in
20+
Skiplist.try_add t key value |> ignore
21+
done;
22+
23+
let n_ops = (100 + percent_mem) * n_ops / 100 in
24+
let n_ops = n_ops * n_domains in
25+
26+
let n_ops_todo = Atomic.make 0 |> Multicore_magic.copy_as_padded in
27+
28+
let init _ =
29+
Atomic.set n_ops_todo n_ops;
30+
Random.State.make_self_init ()
1831
in
19-
let threads = List.init num_threads (fun _ -> push ()) in
20-
let start_time_threads =
21-
List.map (fun domain -> Domain.join domain) threads
32+
let work _ state =
33+
let rec work () =
34+
let n = Util.alloc n_ops_todo in
35+
if n <> 0 then
36+
let rec loop n =
37+
if 0 < n then
38+
let value = Random.State.bits state in
39+
let op = (value asr 20) mod 100 in
40+
let key = value mod n_keys in
41+
if op < limit_mem then begin
42+
Skiplist.mem t key |> ignore;
43+
loop (n - 1)
44+
end
45+
else if op < limit_add then begin
46+
Skiplist.try_add t key value |> ignore;
47+
loop (n - 1)
48+
end
49+
else begin
50+
Skiplist.try_remove t key |> ignore;
51+
loop (n - 1)
52+
end
53+
else work ()
54+
in
55+
loop n
56+
in
57+
work ()
2258
in
23-
let end_time = Unix.gettimeofday () in
24-
let time_diff = end_time -. List.nth start_time_threads 0 in
25-
time_diff
26-
27-
(* A write heavy workload with threads with 50% adds and 50% removes. *)
28-
let write_heavy_workload num_elems num_threads =
29-
workload num_elems num_threads 0.5 0.5
30-
31-
(* A regular workload with 90% reads, 9% adds and 1% removes. *)
32-
let read_heavy_workload num_elems num_threads =
33-
workload num_elems num_threads 0.09 0.01
34-
35-
let moderate_heavy_workload num_elems num_threads =
36-
workload num_elems num_threads 0.2 0.1
37-
38-
let balanced_heavy_workload num_elems num_threads =
39-
workload num_elems num_threads 0.3 0.2
40-
41-
let bench ~workload_type ~num_elems ~num_threads () =
42-
let workload =
43-
if workload_type = "read_heavy" then read_heavy_workload
44-
else if workload_type = "moderate_heavy" then moderate_heavy_workload
45-
else if workload_type = "balanced_heavy" then balanced_heavy_workload
46-
else write_heavy_workload
59+
60+
let config =
61+
Printf.sprintf "%d workers, %d%% mem %d%% add %d%% rem" n_domains
62+
percent_mem percent_add percent_rem
4763
in
48-
let results = ref [] in
49-
for i = 1 to 10 do
50-
let time = workload num_elems num_threads in
51-
if i > 1 then results := time :: !results
52-
done;
53-
let results = List.sort Float.compare !results in
54-
let median_time = List.nth results 4 in
55-
let median_throughput = Float.of_int num_elems /. median_time in
56-
Benchmark_result.create_generic ~median_time ~median_throughput
57-
("atomic_skiplist_" ^ workload_type)
64+
Times.record ~n_domains ~budgetf ~init ~work ()
65+
|> Util.thruput_metrics ~n:n_ops ~singular:"operation" ~config
66+
67+
let run_suite ~budgetf =
68+
Util.cross [ 10; 50; 90 ] [ 1; 2; 4 ]
69+
|> List.concat_map @@ fun (percent_mem, n_domains) ->
70+
run_one ~budgetf ~n_domains ~percent_mem ()

bench/bench_spsc_queue.ml

+26-33
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,30 @@
1-
module Spsc_queue = Saturn.Single_prod_single_cons_queue
1+
open Multicore_bench
2+
module Queue = Saturn.Single_prod_single_cons_queue
23

3-
let item_count = 2_000_000
4+
let run_one ~budgetf ?(size_exponent = 3) ?(n_msgs = 80 * Util.iter_factor) () =
5+
let queue = Queue.create ~size_exponent in
46

5-
let rec try_until_success f =
6-
try f () with Spsc_queue.Full -> try_until_success f
7-
8-
let run () =
9-
let queue = Spsc_queue.create ~size_exponent:3 in
10-
let pusher =
11-
Domain.spawn (fun () ->
12-
let start_time = Unix.gettimeofday () in
13-
for i = 1 to item_count do
14-
try_until_success (fun () -> Spsc_queue.push queue i)
15-
done;
16-
start_time)
7+
let init _ = () in
8+
let work i () =
9+
if i = 0 then
10+
let rec loop n =
11+
if 0 < n then loop (n - Bool.to_int (Queue.try_push queue n))
12+
in
13+
loop n_msgs
14+
else
15+
let rec loop n =
16+
if 0 < n then
17+
match Queue.pop_opt queue with
18+
| Some _ -> loop (n - 1)
19+
| None -> loop n
20+
in
21+
loop n_msgs
1722
in
18-
for _ = 1 to item_count do
19-
while Option.is_none (Spsc_queue.pop_opt queue) do
20-
()
21-
done
22-
done;
23-
let end_time = Unix.gettimeofday () in
24-
let start_time = Domain.join pusher in
25-
let time_diff = end_time -. start_time in
26-
time_diff
2723

28-
let bench () =
29-
let results = ref [] in
30-
for i = 1 to 10 do
31-
let time = run () in
32-
if i > 1 then results := time :: !results
33-
done;
34-
let results = List.sort Float.compare !results in
35-
let median_time = List.nth results 4 in
36-
let median_throughput = Float.of_int item_count /. median_time in
37-
Benchmark_result.create_generic ~median_time ~median_throughput "spsc-queue"
24+
let config = Printf.sprintf "2 workers, capacity %d" (1 lsl size_exponent) in
25+
Times.record ~n_domains:2 ~budgetf ~init ~work ()
26+
|> Util.thruput_metrics ~n:n_msgs ~singular:"message" ~config
27+
28+
let run_suite ~budgetf =
29+
[ 0; 3; 6; 9 ]
30+
|> List.concat_map @@ fun size_exponent -> run_one ~budgetf ~size_exponent ()

0 commit comments

Comments
 (0)