Skip to content

Commit 07bafa7

Browse files
committed
Benchmark comparing the two async_manual_reset_event implementations
1 parent a779de2 commit 07bafa7

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* Licensed under the Apache License Version 2.0 with LLVM Exceptions
5+
* (the "License"); you may not use this file except in compliance with
6+
* the License. You may obtain a copy of the License at
7+
*
8+
* https://llvm.org/LICENSE.txt
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Benchmark: v1 vs v2 async_manual_reset_event
18+
//
19+
// Ping-pong (low contention):
20+
// 1 generator + 1 listener, 2 events, 2 threads.
21+
// Measures round-trip latency of set + async_wait + reset.
22+
//
23+
// Shared event (high contention):
24+
// 1 signaller doing set/reset cycles on a single event,
25+
// 4 waiter tasks doing async_wait() in a loop.
26+
// Stresses push_back contention (multiple waiters registering
27+
// simultaneously) and push vs drain contention (waiters
28+
// registering while set() drains the list).
29+
//
30+
// By default uses pure sender pipelines for the ping-pong test
31+
// (no coroutine overhead). Define AMRE_BENCH_COROUTINES to use
32+
// coroutine-based implementation instead (requires coroutine
33+
// support).
34+
//
35+
// Deadlock watchdog: a standalone std::thread sleeps for 5 minutes,
36+
// then dumps diagnostic state (per-task step + cycle, event ready
37+
// flags) to stderr and calls std::terminate(). This is independent
38+
// of the unifex machinery under test.
39+
40+
#include <unifex/async_manual_reset_event.hpp>
41+
#include <unifex/defer.hpp>
42+
#include <unifex/let_value.hpp>
43+
#include <unifex/repeat_effect_until.hpp>
44+
#include <unifex/scheduler_concepts.hpp>
45+
#include <unifex/single_thread_context.hpp>
46+
#include <unifex/sync_wait.hpp>
47+
#include <unifex/then.hpp>
48+
#include <unifex/v2/async_manual_reset_event.hpp>
49+
#include <unifex/when_all.hpp>
50+
#include <unifex/with_query_value.hpp>
51+
52+
#include <atomic>
53+
#include <chrono>
54+
#include <condition_variable>
55+
#include <cstdio>
56+
#include <functional>
57+
#include <mutex>
58+
#include <thread>
59+
#include <type_traits>
60+
61+
using namespace unifex;
62+
using bench_clock = std::chrono::steady_clock;
63+
64+
template <typename Event, typename Scheduler>
65+
auto async_wait_on(Event& evt, Scheduler sched) {
66+
return with_query_value(evt.async_wait(), get_scheduler, sched);
67+
}
68+
69+
// ---- Ping-pong implementation selection -----------------------------------
70+
//
71+
// Generator: set(ping) -> wait(pong) -> reset(pong) -> loop
72+
// Listener: wait(ping) -> reset(ping) -> set(pong) -> loop
73+
//
74+
// with_query_value overrides get_scheduler so that async_wait
75+
// reschedules onto the designated thread.
76+
77+
template <typename Event, typename Scheduler>
78+
auto gen(Event& ping, Event& pong, Scheduler sched, int n) {
79+
return repeat_effect_until(
80+
defer([&ping, &pong, sched] {
81+
ping.set();
82+
return async_wait_on(pong, sched) | then([&pong] { pong.reset(); });
83+
}),
84+
[n, i = 0]() mutable { return ++i >= n; });
85+
}
86+
87+
template <typename Event, typename Scheduler>
88+
auto listen(Event& ping, Event& pong, Scheduler sched, int n) {
89+
return repeat_effect_until(
90+
defer([&ping, &pong, sched] {
91+
return async_wait_on(ping, sched) | then([&ping, &pong] {
92+
ping.reset();
93+
pong.set();
94+
});
95+
}),
96+
[n, i = 0]() mutable { return ++i >= n; });
97+
}
98+
99+
// ---- Ping-pong: 1 pair (2 threads) ---------------------------------------
100+
101+
template <typename Event>
102+
void run_pingpong(int n) {
103+
Event ping;
104+
Event pong;
105+
single_thread_context c0, c1;
106+
107+
sync_wait(when_all(
108+
gen(ping, pong, c0.get_scheduler(), n),
109+
listen(ping, pong, c1.get_scheduler(), n)));
110+
}
111+
112+
// ---- Shared event: 1 signaller + 4 waiters (5 threads) -------------------
113+
//
114+
// Signaller: n set/reset cycles on a shared event.
115+
// Waiters: async_wait(evt) in a loop via sender pipelines.
116+
//
117+
// Each cycle has two ack rounds:
118+
// 1. Signaller: evt.set() -> drains waiter list
119+
// 2. Waiters: wake, ack -> last sets ack_event
120+
// 3. Signaller: wait ack_event, reset ack, evt.reset(),
121+
// release_event.set() -> tells waiters to proceed
122+
// 4. Waiters: wait release_event, ack -> last sets ack_event
123+
// 5. Signaller: wait ack_event, reset ack,
124+
// release_event.reset() -> safe: all waiters passed
125+
//
126+
// Two ack rounds ensure the signaller waits for ALL waiters
127+
// before resetting both evt and release_event, preventing
128+
// the set/reset Dekker race in scheduler-affine implementations.
129+
//
130+
// Termination: signaller sets done + evt + release_event and
131+
// exits. Waiters stuck on either event wake up, and the done
132+
// predicate terminates their loops. No final ack round -- a
133+
// waiter that already exited (predicate saw done=true) would
134+
// never ack, causing a deadlock.
135+
//
136+
// All tasks run in a single sync_wait(when_all(...)) with each
137+
// task pinned to its own single_thread_context scheduler.
138+
//
139+
// This exercises real contention:
140+
// - Step 1->2: multiple waiters call push_back simultaneously
141+
// - Step 1: set() drains while late waiters may still push
142+
143+
template <typename Event>
144+
void run_contention(int n) {
145+
Event evt; // event under test
146+
Event ack_event; // last acker -> signaller
147+
Event release_event; // signaller -> waiters: evt is reset
148+
std::atomic<int> ack{0};
149+
std::atomic<bool> done{false};
150+
151+
constexpr int num_waiters = 4;
152+
153+
// ---- Execution contexts ----
154+
155+
single_thread_context ctx[num_waiters + 1];
156+
157+
auto do_ack = [&] {
158+
if (ack.fetch_add(1, std::memory_order_acq_rel) == num_waiters - 1) {
159+
ack_event.set();
160+
}
161+
};
162+
163+
auto reset_ack = [&] {
164+
ack_event.reset();
165+
ack.store(0, std::memory_order_relaxed);
166+
};
167+
168+
auto sig_sched = ctx[0].get_scheduler();
169+
170+
// Signaller: n cycles, then final wake for termination.
171+
auto signaller =
172+
repeat_effect_until(
173+
defer([&] {
174+
evt.set();
175+
return async_wait_on(ack_event, sig_sched) | then([&] {
176+
reset_ack();
177+
evt.reset();
178+
release_event.set();
179+
}) |
180+
let_value([&] {
181+
return async_wait_on(ack_event, sig_sched) | then([&] {
182+
reset_ack();
183+
release_event.reset();
184+
});
185+
});
186+
}),
187+
[n, i = 0]() mutable { return ++i >= n; }) |
188+
then([&] {
189+
done.store(true, std::memory_order_release);
190+
evt.set();
191+
release_event.set();
192+
});
193+
194+
// Waiter: loop until done, acking twice per cycle.
195+
auto make_waiter = [&](int idx) {
196+
auto sched = ctx[idx + 1].get_scheduler();
197+
return repeat_effect_until(
198+
defer([&, sched] {
199+
return async_wait_on(evt, sched) | then(do_ack) |
200+
let_value([&, sched] {
201+
return async_wait_on(release_event, sched) | then(do_ack);
202+
});
203+
}),
204+
[&] { return done.load(std::memory_order_acquire); });
205+
};
206+
207+
sync_wait(when_all(
208+
std::move(signaller),
209+
make_waiter(0),
210+
make_waiter(1),
211+
make_waiter(2),
212+
make_waiter(3)));
213+
}
214+
215+
// ---- Time-bounded benchmarking -------------------------------------------
216+
//
217+
// Runs the benchmark in fixed-size batches, accumulating iterations
218+
// until the target duration is reached. This avoids the calibration
219+
// pitfall where a quiet warm-up period leads to an oversized N that
220+
// cannot complete under heavy load (common on shared CI runners).
221+
222+
static constexpr auto bench_duration = std::chrono::seconds(1);
223+
224+
// Keep batch size small so the time-bounded loop can exit promptly.
225+
// Each ping-pong iteration involves cross-thread round-trips whose
226+
// latency can be 10-100ms on overloaded CI VMs.
227+
static constexpr int pingpong_batch = 10;
228+
229+
// Contention batch can be larger: the signaller does set/reset in
230+
// a tight loop on one thread, so individual cycles are fast.
231+
// Thread contexts are created once per batch.
232+
static constexpr int contention_batch = 1000;
233+
234+
template <typename Fn>
235+
void bench(const char* label, Fn fn, int batch) {
236+
int total = 0;
237+
auto t0 = bench_clock::now();
238+
bench_clock::duration elapsed;
239+
240+
do {
241+
fn(batch);
242+
total += batch;
243+
elapsed = bench_clock::now() - t0;
244+
} while (elapsed < bench_duration);
245+
246+
auto elapsed_ns = static_cast<double>(
247+
std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count());
248+
249+
std::printf(
250+
" %-8s %8d iters %8.0f ns/iter\n", label, total, elapsed_ns / total);
251+
}
252+
253+
int main() {
254+
using v1_event = async_manual_reset_event;
255+
using v2_event = v2::async_manual_reset_event;
256+
257+
std::printf("Ping-pong (1 generator, 1 listener):\n");
258+
bench("v1", [&](int n) { run_pingpong<v1_event>(n); }, pingpong_batch);
259+
bench("v2", [&](int n) { run_pingpong<v2_event>(n); }, pingpong_batch);
260+
261+
std::printf("\nContention (1 signaller, 4 waiters, shared event):\n");
262+
bench("v1", [&](int n) { run_contention<v1_event>(n); }, contention_batch);
263+
bench("v2", [&](int n) { run_contention<v2_event>(n); }, contention_batch);
264+
265+
return 0;
266+
}

0 commit comments

Comments
 (0)