Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Allocation-free ready semaphore acquires**: Non-cancellable acquires now
defer shared wake-state allocation until the ready permit check fails.
Entering `await_suspend` allocates before taking the semaphore queue lock,
even if its locked recheck consumes a released permit. This preserves permit
accounting and dequeue-versus-frame-destruction safety. Token-aware acquires
retain eager cancellation state (#1039).
- **Allocation-free uncontended mutex locks**: Non-cancellable locks now defer
shared wake-state allocation until both the initial acquisition and the
suspension-entry recheck observe contention. Truly parked waiters retain
Expand Down
131 changes: 123 additions & 8 deletions examples/microbench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <elio/log/macros.hpp>
#include <elio/sync/event.hpp>
#include <elio/sync/mutex.hpp>
#include <elio/sync/semaphore.hpp>
#include <atomic>
#include <cstdlib>
#include <iostream>
Expand Down Expand Up @@ -47,6 +48,22 @@ coro::task<void> mutex_handoffs(sync::mutex& mutex, size_t iterations) {
}
}

coro::task<void> ready_semaphore_acquires(sync::semaphore& semaphore,
size_t iterations) {
for (size_t i = 0; i < iterations; ++i) {
co_await semaphore.acquire();
semaphore.release();
}
}

coro::task<void> semaphore_handoffs(sync::semaphore& semaphore,
size_t iterations) {
for (size_t i = 0; i < iterations; ++i) {
co_await semaphore.acquire();
semaphore.release();
}
}

int main() {
log::logger::instance().set_level(log::level::error);

Expand Down Expand Up @@ -217,7 +234,105 @@ int main() {
<< " ns/handoff" << std::endl;
}

// 7. Measure MPSC push only (no scheduler overhead)
// 7. Measure ready semaphore acquire/release. No waiter is published, so
// release() does not reserve storage for a wake-state vector. One
// long-lived frame keeps coroutine construction outside the timed loop.
{
constexpr size_t acquire_iterations = 1000000;
sync::semaphore semaphore(1);
auto acquires =
ready_semaphore_acquires(semaphore, acquire_iterations);
auto handle = coro::detail::task_access::handle(acquires);

auto start = high_resolution_clock::now();
{
coro::detail::frame_context_scope frame_scope(
std::addressof(handle.promise()));
handle.resume();
}
auto end = high_resolution_clock::now();
if (!handle.done() || semaphore.count() != 1) {
std::abort();
}
auto ns = duration_cast<nanoseconds>(end - start).count();

std::cout << "Ready semaphore acquire/release: "
<< (static_cast<double>(ns) / acquire_iterations)
<< " ns/iteration" << std::endl;
}

// 8. Isolate construction, publication, and unlinking of a parked
// semaphore waiter without release()'s existing wake-vector allocation.
// This is a mechanism diagnostic rather than an application throughput
// benchmark.
{
constexpr size_t park_iterations = 200000;
sync::semaphore semaphore(0);

auto start = high_resolution_clock::now();
for (size_t i = 0; i < park_iterations; ++i) {
auto waiter = semaphore.acquire();
if (waiter.await_ready() ||
!waiter.await_suspend(std::noop_coroutine())) {
std::abort();
}
}
auto end = high_resolution_clock::now();
if (semaphore.count() != 0) {
std::abort();
}
auto ns = duration_cast<nanoseconds>(end - start).count();

std::cout << "Semaphore park/unlink diagnostic: "
<< (static_cast<double>(ns) / park_iterations)
<< " ns/iteration" << std::endl;
}

// 9. Measure forced permit handoff between two long-lived frames. Each
// release of an already-published waiter also exercises release()'s
// existing one-element wake-vector allocation, so the park/unlink result
// above remains the allocation-codegen control.
{
constexpr size_t handoff_iterations_per_task = 100000;
constexpr size_t total_handoffs = handoff_iterations_per_task * 2;
sync::semaphore semaphore(0);

auto first =
semaphore_handoffs(semaphore, handoff_iterations_per_task);
auto second =
semaphore_handoffs(semaphore, handoff_iterations_per_task);
auto first_handle = coro::detail::task_access::handle(first);
auto second_handle = coro::detail::task_access::handle(second);
{
coro::detail::frame_context_scope frame_scope(
std::addressof(first_handle.promise()));
first_handle.resume();
}
{
coro::detail::frame_context_scope frame_scope(
std::addressof(second_handle.promise()));
second_handle.resume();
}
if (first_handle.done() || second_handle.done()) {
std::abort();
}

auto start = high_resolution_clock::now();
semaphore.release();
auto end = high_resolution_clock::now();
if (!first_handle.done() || !second_handle.done() ||
semaphore.count() != 1 || !semaphore.try_acquire() ||
semaphore.count() != 0) {
std::abort();
}
auto ns = duration_cast<nanoseconds>(end - start).count();

std::cout << "Forced two-task semaphore handoff: "
<< (static_cast<double>(ns) / total_handoffs)
<< " ns/handoff" << std::endl;
}

// 10. Measure MPSC push only (no scheduler overhead)
{
runtime::mpsc_queue<void> queue;

Expand All @@ -234,7 +349,7 @@ int main() {
while (queue.pop()) {}
}

// 8. Measure Chase-Lev push only
// 11. Measure Chase-Lev push only
{
runtime::chase_lev_deque<void> queue;

Expand All @@ -251,7 +366,7 @@ int main() {
while (queue.pop()) {}
}

// 9. Compare atomic RMW with single-writer snapshot publication
// 12. Compare atomic RMW with single-writer snapshot publication
{
std::atomic<size_t> published{0};

Expand Down Expand Up @@ -283,7 +398,7 @@ int main() {
<< " ns/update" << std::endl;
}

// 10. Compare exact timestamps with the disabled diagnostic fast path
// 13. Compare exact timestamps with the disabled diagnostic fast path
{
std::atomic<steady_clock::time_point> last_task_time{
steady_clock::now()};
Expand Down Expand Up @@ -318,7 +433,7 @@ int main() {
<< " ns/update" << std::endl;
}

// 11. Measure atomic fence alone
// 14. Measure atomic fence alone
{
auto start = high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
Expand All @@ -330,7 +445,7 @@ int main() {
std::cout << "Atomic release fence: " << (ns / N) << " ns" << std::endl;
}

// 12. Measure eventfd write
// 15. Measure eventfd write
{
int fd = eventfd(0, EFD_NONBLOCK);
uint64_t val = 1;
Expand All @@ -346,7 +461,7 @@ int main() {
close(fd);
}

// 13. Full spawn path (with running scheduler) - includes alloc + spawn
// 16. Full spawn path (with running scheduler) - includes alloc + spawn
{
runtime::scheduler sched(4);
sched.start();
Expand All @@ -368,7 +483,7 @@ int main() {
sched.shutdown();
}

// 14. Measure warmed-up worker overhead
// 17. Measure warmed-up worker overhead
{
runtime::scheduler sched(4);
sched.start();
Expand Down
Loading