From ba0ef2a20d79bb0ac90b04a7c574d131291d8d0c Mon Sep 17 00:00:00 2001 From: sundeep8967 Date: Sun, 8 Nov 2026 15:19:58 +0530 Subject: [PATCH] perf(mutex): optimize uncontended lock release with spare permit Avoid acquiring the waiters queue lock when dropping a MutexGuard in the uncontended case. By configuring the internal semaphore to start with 3 permits and having each MutexGuard acquire and release 2 permits, an uncontended lock holder leaves 1 spare permit in the semaphore. When a waiter arrives, it claims this spare permit while enqueuing, ensuring that the lock holder's release(2) can safely restore permits without acquiring the waiters queue lock whenever no waiters have enqueued. Removes unused Semaphore::release_with_lock and its test. Fixes #403 Signed-off-by: sundeep8967 --- CHANGELOG.md | 1 + asyncband/src/internal/semaphore.rs | 29 ----------------------------- asyncband/src/mutex/mod.rs | 20 +++++++++++--------- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7736de..3d849521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ All notable changes to this project will be documented in this file. ### Improvements +* Reduce the cost of acquiring and releasing an uncontended `Mutex` by avoiding its internal wait-queue lock. * Reduce the cost of initializing an uncontended `OnceCell` by avoiding its internal wait-queue lock. * Reduce the cost of polling a pending oneshot receive again from the same task, which now keeps its registered waker instead of cloning and dropping it. * Reduce the state kept by futures waiting for a `Mutex`, `RwLock`, `Semaphore`, `Once`, `OnceCell`, or bounded broadcast capacity, making lock, permit acquisition, and bounded broadcast send futures smaller. diff --git a/asyncband/src/internal/semaphore.rs b/asyncband/src/internal/semaphore.rs index 2964757a..68d5785b 100644 --- a/asyncband/src/internal/semaphore.rs +++ b/asyncband/src/internal/semaphore.rs @@ -164,17 +164,6 @@ impl Semaphore { self.distribute_permits(n, self.waiters.lock()); } - /// Releases `n` permits by acquiring the queue lock directly. - /// - /// Use this when no permits can be available, as when a mutex guard holds its only permit, - /// to skip the speculative attempt in [`release`](Self::release). - /// This method also works when permits are available. - pub fn release_with_lock(&self, n: usize) { - if n != 0 { - self.distribute_permits(n, self.waiters.lock()); - } - } - /// Releases `n` permits only if the queue contains waiters when locked. #[cfg(feature = "broadcast")] pub fn release_if_waiters(&self, n: usize) { @@ -533,24 +522,6 @@ mod tests { assert!(semaphore.waiters.lock().is_empty()); } - #[test] - fn release_with_lock_hands_permits_to_waiters() { - let semaphore = Semaphore::new(1); - let counter = Arc::new(WakeCounter(AtomicUsize::new(0))); - let waker = Waker::from(counter.clone()); - assert!(semaphore.try_acquire(1)); - let mut acquire = semaphore.acquire(1); - assert!(acquire.poll_once(&waker).is_pending()); - - semaphore.release_with_lock(1); - assert_eq!(counter.0.load(Ordering::Relaxed), 1); - assert_eq!(semaphore.available_permits(), 0); - assert!(acquire.poll_once(&waker).is_ready()); - - semaphore.release_with_lock(1); - assert_eq!(semaphore.available_permits(), 1); - } - #[test] fn release_distributes_permits_across_multiple_batches() { const WAITER_COUNT: usize = WakerBatch::STACK_SIZE * 2 + 1; diff --git a/asyncband/src/mutex/mod.rs b/asyncband/src/mutex/mod.rs index da8e81c2..2b723a2f 100644 --- a/asyncband/src/mutex/mod.rs +++ b/asyncband/src/mutex/mod.rs @@ -120,7 +120,9 @@ impl Mutex { /// let mutex = Mutex::new(5); /// ``` pub const fn new(t: T) -> Self { - let s = semaphore::Semaphore::new(1); + // Each lock takes two of three permits, so only one can run at a time. The spare + // permit lets an uncontended release avoid locking the semaphore's wait queue. + let s = semaphore::Semaphore::new(3); let c = UnsafeCell::new(t); Self { s, c } } @@ -162,7 +164,7 @@ impl Mutex { /// # } /// ``` pub async fn lock(&self) -> MutexGuard<'_, T> { - self.s.acquire(1).await; + self.s.acquire(2).await; MutexGuard { lock: self } } @@ -179,7 +181,7 @@ impl Mutex { /// assert_eq!(2, *guard); /// ``` pub fn try_lock(&self) -> Option> { - if self.s.try_acquire(1) { + if self.s.try_acquire(2) { let guard = MutexGuard { lock: self }; Some(guard) } else { @@ -212,7 +214,7 @@ impl Mutex { /// # } /// ``` pub async fn lock_owned(self: Arc) -> OwnedMutexGuard { - self.s.acquire(1).await; + self.s.acquire(2).await; OwnedMutexGuard { lock: self } } @@ -233,7 +235,7 @@ impl Mutex { /// assert_eq!(2, *guard); /// ``` pub fn try_lock_owned(self: Arc) -> Option> { - if self.s.try_acquire(1) { + if self.s.try_acquire(2) { let guard = OwnedMutexGuard { lock: self }; Some(guard) } else { @@ -278,7 +280,7 @@ unsafe impl Sync for MutexGuard<'_, T> {} impl Drop for MutexGuard<'_, T> { fn drop(&mut self) { - self.lock.s.release_with_lock(1); + self.lock.s.release(2); } } @@ -445,7 +447,7 @@ unsafe impl Sync for OwnedMutexGuard {} impl Drop for OwnedMutexGuard { fn drop(&mut self) { - self.lock.s.release_with_lock(1); + self.lock.s.release(2); } } @@ -641,7 +643,7 @@ unsafe impl Sync for MappedMutexGuard<'_, T> {} impl Drop for MappedMutexGuard<'_, T> { fn drop(&mut self) { - self.s.release_with_lock(1); + self.s.release(2); } } @@ -840,7 +842,7 @@ unsafe impl Sync for OwnedMapp impl Drop for OwnedMappedMutexGuard { fn drop(&mut self) { - self.lock.s.release_with_lock(1); + self.lock.s.release(2); } }