Skip to content

Commit 2e2ded8

Browse files
committed
perf(mpmc): back bounded queues with a ring buffer
Replace the bounded queue's VecDeque behind one mutex with a preallocated ring of stamped slots adapted from crossbeam-channel's array flavor. Sends, receives, and reservations take no lock unless a sender or receiver has to wait, while wait-queue order, exact capacity, permits, cancellation, and disconnection behave as before. Storage for the full capacity is allocated when the queue is created. The capacity that permits and grants hold, and a bit marking queued senders, live in the tail cursor beside the position, so every exchange on it compares the reservation state it was computed from. Positions use 64 bits wherever 64-bit atomics exist, as in crossbeam-channel. While senders are queued, a send with a permit grants the capacity that receivers free until it finishes, so a single receiver does not have to wake every queued sender itself. Copy CachePadded from crossbeam-utils and record the incorporated crossbeam portions in LICENSE, licenserc.toml, and .asf.yaml.
1 parent 7b32ce7 commit 2e2ded8

14 files changed

Lines changed: 1853 additions & 326 deletions

File tree

‎.asf.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ project:
4141
source_excludes_lightweight:
4242
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/blocking/executor.rs"
4343
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/blocking/parker.rs"
44+
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/internal/cache_padded.rs"
4445
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/pool/bounded.rs"
4546
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/pool/common.rs"
4647
- "/apache-asyncband-*-incubating-src.tar.gz/*/asyncband/src/pool/mod.rs"

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ All notable changes to this project will be documented in this file.
4040

4141
### Improvements
4242

43+
* Bounded MPMC sends, receives, and reservations no longer take a lock unless a sender or receiver is waiting, which speeds up contended workloads on threads and async runtimes. Creating a bounded queue now allocates storage for its full capacity, and handoffs where every send waits for a receiver, such as through a capacity-1 queue, are slightly slower.
4344
* Improve `FutureExt::block_on` throughput and reduce context switches when many threads repeatedly block on the same primitive, such as a contended MPMC channel; an isolated cross-thread wake-up costs slightly more.
4445
* Reduce `ManualResetEvent` state-query and already-set wait overhead, especially with concurrent readers.
4546
* Add a lock-free fast path for releasing `Semaphore` permits while permits remain available, which speeds up dropping `RwLock` read guards, releasing multi-permit `Semaphore` permits, and returning objects to a bounded pool that is not at capacity. `Mutex` releases are unchanged.

‎LICENSE‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,22 @@ the following copyright notice:
416416

417417
Copyright 2014-2020 The Rust Project Developers
418418

419+
Portions of asyncband/src/internal/cache_padded.rs are adapted from
420+
crossbeam-utils 0.8.23. Portions of the slot-stamp algorithm and backoff helper
421+
in asyncband/src/mpmc/ring.rs are adapted from crossbeam-channel 0.5.17 and
422+
crossbeam-utils 0.8.23. Both are taken from the following exact revision and
423+
source paths:
424+
425+
https://github.com/crossbeam-rs/crossbeam/blob/2920c984290229ab4e0ca0452ef09e48a82063f3/crossbeam-utils/src/cache_padded.rs
426+
https://github.com/crossbeam-rs/crossbeam/blob/2920c984290229ab4e0ca0452ef09e48a82063f3/crossbeam-utils/src/backoff.rs
427+
https://github.com/crossbeam-rs/crossbeam/blob/2920c984290229ab4e0ca0452ef09e48a82063f3/crossbeam-channel/src/flavors/array.rs
428+
429+
Crossbeam is licensed under Apache-2.0 or MIT. Apache Asyncband uses the
430+
Apache-2.0 option for the incorporated portions. The upstream project carries
431+
the following copyright notice:
432+
433+
Copyright (c) 2019 The Crossbeam Project Developers
434+
419435
Portions of the following files are ported from Fastpool 1.1.1. Each local path
420436
is followed by its upstream source path or paths:
421437

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// This file contains code adapted from crossbeam-utils 0.8.23.
19+
// Copyright (c) 2019 The Crossbeam Project Developers
20+
// Asyncband uses the upstream project's Apache-2.0 license option for the incorporated code.
21+
// The incorporated code has been modified for use in Apache Asyncband: examples, conversions, and
22+
// explicit Send and Sync implementations are omitted, and imports follow Asyncband's style.
23+
// Upstream source:
24+
// https://github.com/crossbeam-rs/crossbeam/blob/2920c984290229ab4e0ca0452ef09e48a82063f3/crossbeam-utils/src/cache_padded.rs
25+
26+
use std::fmt;
27+
use std::ops::Deref;
28+
use std::ops::DerefMut;
29+
30+
/// Pads and aligns a value to the length of a cache line.
31+
///
32+
/// In concurrent programming, sometimes it is desirable to make sure commonly accessed pieces of
33+
/// data are not placed into the same cache line. Updating an atomic value invalidates the whole
34+
/// cache line it belongs to, which makes the next access to the same cache line slower for other
35+
/// CPU cores. Use `CachePadded` to ensure updating one piece of data doesn't invalidate other
36+
/// cached data.
37+
///
38+
/// # Size and alignment
39+
///
40+
/// Cache lines are assumed to be N bytes long, depending on the architecture:
41+
///
42+
/// * On x86-64, aarch64, and powerpc64, N = 128.
43+
/// * On arm, mips, mips64, sparc, and hexagon, N = 32.
44+
/// * On m68k, N = 16.
45+
/// * On s390x, N = 256.
46+
/// * On all others, N = 64.
47+
///
48+
/// Note that N is just a reasonable guess and is not guaranteed to match the actual cache line
49+
/// length of the machine the program is running on. On modern Intel architectures, spatial
50+
/// prefetcher is pulling pairs of 64-byte cache lines at a time, so we pessimistically assume that
51+
/// cache lines are 128 bytes long.
52+
///
53+
/// The size of `CachePadded<T>` is the smallest multiple of N bytes large enough to accommodate
54+
/// a value of type `T`.
55+
///
56+
/// The alignment of `CachePadded<T>` is the maximum of N bytes and the alignment of `T`.
57+
///
58+
/// # Layout
59+
///
60+
/// This type is `#[repr(C)]`, so a pointer to `CachePadded<T>` has the same address as the pointer
61+
/// to the underlying `T`.
62+
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)]
63+
// Starting from Intel's Sandy Bridge, spatial prefetcher is now pulling pairs of 64-byte cache
64+
// lines at a time, so we have to align to 128 bytes rather than 64.
65+
//
66+
// Sources:
67+
// - https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
68+
// - https://github.com/facebook/folly/blob/1b5288e6eea6df074758f877c849b6e73bbb9fbb/folly/lang/Align.h#L107
69+
//
70+
// aarch64/arm64ec's big.LITTLE architecture has asymmetric cores and "big" cores have 128-byte
71+
// cache line size.
72+
//
73+
// Sources:
74+
// - https://www.mono-project.com/news/2016/09/12/arm64-icache/
75+
//
76+
// powerpc64 has 128-byte cache line size.
77+
//
78+
// Sources:
79+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_ppc64x.go#L9
80+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/powerpc/include/asm/cache.h#L26
81+
#[cfg_attr(
82+
any(
83+
target_arch = "x86_64",
84+
target_arch = "aarch64",
85+
target_arch = "arm64ec",
86+
target_arch = "powerpc64",
87+
),
88+
repr(align(128))
89+
)]
90+
// arm, mips, mips64, sparc, and hexagon have 32-byte cache line size.
91+
//
92+
// Sources:
93+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_arm.go#L7
94+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mips.go#L7
95+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mipsle.go#L7
96+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_mips64x.go#L9
97+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/sparc/include/asm/cache.h#L17
98+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/hexagon/include/asm/cache.h#L12
99+
#[cfg_attr(
100+
any(
101+
target_arch = "arm",
102+
target_arch = "mips",
103+
target_arch = "mips32r6",
104+
target_arch = "mips64",
105+
target_arch = "mips64r6",
106+
target_arch = "sparc",
107+
target_arch = "hexagon",
108+
),
109+
repr(align(32))
110+
)]
111+
// m68k has 16-byte cache line size.
112+
//
113+
// Sources:
114+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/m68k/include/asm/cache.h#L9
115+
#[cfg_attr(target_arch = "m68k", repr(align(16)))]
116+
// s390x has 256-byte cache line size.
117+
//
118+
// Sources:
119+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_s390x.go#L7
120+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/s390/include/asm/cache.h#L13
121+
#[cfg_attr(target_arch = "s390x", repr(align(256)))]
122+
// x86, wasm, riscv, and sparc64 have 64-byte cache line size.
123+
//
124+
// Sources:
125+
// - https://github.com/golang/go/blob/dda2991c2ea0c5914714469c4defc2562a907230/src/internal/cpu/cpu_x86.go#L9
126+
// - https://github.com/golang/go/blob/3dd58676054223962cd915bb0934d1f9f489d4d2/src/internal/cpu/cpu_wasm.go#L7
127+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/riscv/include/asm/cache.h#L10
128+
// - https://github.com/torvalds/linux/blob/3516bd729358a2a9b090c1905bd2a3fa926e24c6/arch/sparc/include/asm/cache.h#L19
129+
//
130+
// All others are assumed to have 64-byte cache line size.
131+
#[cfg_attr(
132+
not(any(
133+
target_arch = "x86_64",
134+
target_arch = "aarch64",
135+
target_arch = "arm64ec",
136+
target_arch = "powerpc64",
137+
target_arch = "arm",
138+
target_arch = "mips",
139+
target_arch = "mips32r6",
140+
target_arch = "mips64",
141+
target_arch = "mips64r6",
142+
target_arch = "sparc",
143+
target_arch = "hexagon",
144+
target_arch = "m68k",
145+
target_arch = "s390x",
146+
)),
147+
repr(align(64))
148+
)]
149+
#[repr(C)]
150+
pub struct CachePadded<T> {
151+
value: T,
152+
}
153+
154+
impl<T> CachePadded<T> {
155+
/// Pads and aligns a value to the length of a cache line.
156+
pub const fn new(t: T) -> CachePadded<T> {
157+
CachePadded::<T> { value: t }
158+
}
159+
}
160+
161+
impl<T> Deref for CachePadded<T> {
162+
type Target = T;
163+
164+
fn deref(&self) -> &T {
165+
&self.value
166+
}
167+
}
168+
169+
impl<T> DerefMut for CachePadded<T> {
170+
fn deref_mut(&mut self) -> &mut T {
171+
&mut self.value
172+
}
173+
}
174+
175+
impl<T: fmt::Debug> fmt::Debug for CachePadded<T> {
176+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177+
f.debug_struct("CachePadded")
178+
.field("value", &self.value)
179+
.finish()
180+
}
181+
}

‎asyncband/src/internal/mod.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub fn register_waker(slot: &mut Option<Waker>, waker: &Waker) -> Option<Waker>
5454
#[allow(dead_code)]
5555
pub(crate) mod arena;
5656

57+
#[cfg(feature = "mpmc")]
58+
pub(crate) mod cache_padded;
59+
5760
#[cfg(any(feature = "latch", feature = "once"))]
5861
pub(crate) mod countdown;
5962

@@ -116,6 +119,7 @@ pub(crate) mod waitlist;
116119
feature = "completion",
117120
feature = "event",
118121
feature = "latch",
122+
feature = "mpmc",
119123
feature = "mutex",
120124
feature = "once",
121125
feature = "phaser",

‎asyncband/src/internal/waker_batch.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ impl WakerBatch {
7575
// SAFETY: This prefix was initialized by `push` and is no longer owned by the batch.
7676
unsafe { slot.assume_init_read() }.wake();
7777
}
78-
self.spilled.drain(..).for_each(Waker::wake);
78+
if !self.spilled.is_empty() {
79+
self.spilled.drain(..).for_each(Waker::wake);
80+
}
7981
}
8082
}
8183

‎asyncband/src/mpmc/bounded.rs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,26 @@ use crate::mpmc::RecvError;
2222
use crate::mpmc::SendError;
2323
use crate::mpmc::TryRecvError;
2424
use crate::mpmc::TrySendError;
25-
use crate::mpmc::queue::Permit;
26-
use crate::mpmc::queue::Shared;
25+
use crate::mpmc::ring::Permit;
26+
use crate::mpmc::ring::Shared;
2727

2828
/// Creates a bounded multi-producer, multi-consumer queue.
2929
///
3030
/// Queued values, held permits, and capacity granted to waiting senders occupy at most `capacity`
3131
/// slots. Pending `send` and `reserve` operations receive capacity in wait-queue order. Sending
32-
/// waits for a receiver to free capacity when none is available.
32+
/// waits for a receiver to free capacity when none is available. Storage for `capacity` values is
33+
/// allocated when the queue is created.
3334
///
3435
/// The `try_*` methods do not wait for capacity or messages, but may briefly block on an internal
35-
/// mutex.
36+
/// mutex or spin while another sender or receiver finishes with the same slot.
3637
///
3738
/// # Panics
3839
///
39-
/// Panics if `capacity` is zero.
40+
/// Panics if `capacity` is zero or too large.
4041
#[track_caller]
4142
pub fn bounded<T>(capacity: usize) -> (BoundedSender<T>, BoundedReceiver<T>) {
4243
assert!(capacity > 0, "mpmc bounded queue requires capacity > 0");
43-
let shared = Arc::new(Shared::bounded(capacity));
44+
let shared = Arc::new(Shared::new(capacity));
4445
(
4546
BoundedSender {
4647
shared: shared.clone(),

‎asyncband/src/mpmc/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
mod bounded;
2626
mod error;
2727
mod queue;
28+
mod ring;
2829
mod unbounded;
30+
mod waiters;
2931

3032
pub use self::bounded::BoundedReceiver;
3133
pub use self::bounded::BoundedSender;
@@ -34,7 +36,7 @@ pub use self::error::RecvError;
3436
pub use self::error::SendError;
3537
pub use self::error::TryRecvError;
3638
pub use self::error::TrySendError;
37-
pub use self::queue::Permit;
39+
pub use self::ring::Permit;
3840
pub use self::unbounded::UnboundedReceiver;
3941
pub use self::unbounded::UnboundedSender;
4042
pub use self::unbounded::unbounded;

0 commit comments

Comments
 (0)