Skip to content

perf(mpsc): reduce intermediate copies in bounded receives - #407

Merged
orthur2 merged 4 commits into
apache:mainfrom
sanjay7178:fix-mpsc
Nov 9, 2026
Merged

orthur2 merged 4 commits into
apache:mainfrom
sanjay7178:fix-mpsc

Conversation

@sanjay7178

@sanjay7178 sanjay7178 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reduce intermediate payload copies in bounded MPSC receives by removing the payload/waker tuple from the receive path. Add matched inline and boxed benchmarks and ownership tests for cancellation, disconnection, and resumed sends.

A balanced four-variant benchmark on Linux x86-64 measured the retained receive-only change at 230.5 ms versus 250.5 ms for the inline ready case (16 samples per variant). Contended inline medians were 730 versus 747 ms with broadly overlapping ranges; this does not establish a significant contended throughput improvement. The send-side change has been removed following review. Full methodology and raw samples are in the benchmark follow-up comment.

Related to #401; further work is needed to close the larger throughput gap.

Validation

  • Targeted large-payload ownership test and all 40 MPSC integration tests.
  • cargo x lint.
  • Baseline, send-only, receive-only, and combined benchmark variants with identical fixtures, fixed rounds, and balanced rotating run order.

Signed-off-by: sanjay7178 <saisanjay7660@gmail.com>
@sanjay7178 sanjay7178 changed the title Improve bounded MPSC throughput for inline 1 KiB messages feat(perf) : Improve bounded MPSC throughput for inline 1 KiB messages Nov 4, 2026
@sanjay7178
sanjay7178 marked this pull request as ready for review November 5, 2026 06:15
@orthur2
orthur2 self-requested a review November 8, 2026 02:54

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. The receive-side change looks good to me, but I'd drop the send-side part.

Comment thread asyncband/src/mpsc/bounded/sender.rs Resolved
@sanjay7178

Copy link
Copy Markdown
Contributor Author

Benchmark follow-up

Removed the send hunk as requested; the implementation change now only simplifies receive result handling. The earlier combined before/after measurements did not establish the cause of the contended timing change.

Measured on Linux x86-64 (AMD EPYC-Rome VM, 16 CPUs), rustc 1.99.1 / LLVM 23.1.1, optimized Cargo bench builds. All four variants use base 452d858 plus identical PR benchmark fixtures. Baseline has neither hunk; send-only and receive-only isolate each hunk; combined has both. All builds completed before measurement; no tests or builds ran concurrently. Each case has one warmup per variant followed by 16 recorded launches per variant, rotating and reversing order in balanced blocks. Each launch uses --baseline-only --repeat 1 --raw. No outliers were removed.

Each cell is median ms (min–max); lower is faster.

Scenario Fixed rounds Baseline Send only Receive only (retained) Combined
ready-roundtrip-inline1k 1,000,000 250.5 (242–283) 264 (245–285) 230.5 (219–243) 243 (228–270)
tasks-4p1c-cap64-4w-inline1k 26 747 (634–832) 793 (712–873) 730 (581–837) 793 (557–877)
tasks-4p1c-cap64-4w 26 84 (74–92) 82.5 (74–91) 81 (77–92) 81 (72–87)

The isolated ready case supports dropping the send change on this host. Contended timings remain noisy and platform-dependent; these data do not establish a lock-timing mechanism or resolve #401. The usize filter also matches the inline and boxed rows: those extra rows were executed consistently for every variant, and the table selects only the exact usize row.

Raw recorded times in chronological order within each variant (ms)
ready-roundtrip-inline1k / baseline: [266, 250, 255, 251, 245, 260, 283, 246, 246, 243, 263, 253, 254, 247, 242, 243]
ready-roundtrip-inline1k / send: [251, 264, 245, 272, 260, 269, 285, 272, 254, 264, 269, 256, 260, 267, 260, 270]
ready-roundtrip-inline1k / receive: [243, 243, 223, 230, 242, 234, 239, 219, 241, 227, 227, 231, 227, 229, 234, 221]
ready-roundtrip-inline1k / combined: [228, 260, 248, 230, 241, 245, 254, 270, 236, 248, 231, 247, 246, 238, 228, 231]
tasks-4p1c-cap64-4w-inline1k / baseline: [777, 667, 643, 634, 801, 769, 697, 762, 832, 807, 722, 829, 769, 693, 732, 729]
tasks-4p1c-cap64-4w-inline1k / send: [871, 768, 754, 840, 712, 873, 771, 725, 822, 818, 825, 802, 757, 784, 780, 833]
tasks-4p1c-cap64-4w-inline1k / receive: [704, 690, 581, 751, 601, 810, 778, 759, 632, 744, 699, 818, 716, 630, 837, 819]
tasks-4p1c-cap64-4w-inline1k / combined: [557, 752, 585, 793, 793, 824, 871, 844, 842, 818, 779, 688, 812, 780, 877, 705]
tasks-4p1c-cap64-4w / baseline: [80, 81, 82, 92, 85, 85, 86, 81, 84, 74, 89, 89, 88, 84, 80, 84]
tasks-4p1c-cap64-4w / send: [88, 85, 91, 74, 75, 82, 82, 80, 77, 78, 83, 84, 80, 86, 84, 83]
tasks-4p1c-cap64-4w / receive: [81, 92, 77, 86, 81, 81, 84, 86, 83, 81, 79, 81, 81, 78, 81, 82]
tasks-4p1c-cap64-4w / combined: [82, 81, 72, 82, 79, 87, 85, 81, 77, 74, 78, 82, 83, 81, 79, 76]

@sanjay7178 sanjay7178 changed the title feat(perf) : Improve bounded MPSC throughput for inline 1 KiB messages perf(mpsc): reduce intermediate copies in bounded receives Nov 8, 2026
@orthur2
orthur2 self-requested a review November 9, 2026 03:11

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update, LGTM.

@orthur2
orthur2 merged commit cf7ec96 into apache:main Nov 9, 2026
9 checks passed
@orthur2

orthur2 commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for the update, LGTM.

If you'd like to keep going on #401, feel free to open a follow-up PR.

@sanjay7178

Copy link
Copy Markdown
Contributor Author

Thanks for the update, LGTM.

If you'd like to keep going on #401, feel free to open a follow-up PR.

Sure I'm on it

@sanjay7178

Copy link
Copy Markdown
Contributor Author

Following up on the suggestion to continue #401, I tested several local designs after this PR merged. The most promising prototype uses receiver-local batches and atomic capacity returns for inline payloads of at least 1 KiB. These are results from a separate, unpublished experiment; the implementation merged in #407 remains the baseline.

The receiver swaps buffers under the shared mutex and extracts payloads outside it. Each receive releases exactly one logical capacity unit. Capacity can be returned atomically when positive availability implies no linked sender waiters; otherwise it uses the existing locked FIFO grant path. Smaller payloads retain the original queue and plain capacity arithmetic.

I also tested batching alone, applying the atomic path to all payload sizes, and replacing the mutex with parking_lot 0.12.5. Batching alone was inconclusive; the universal atomic path regressed small-message cases; the mutex replacement regressed the target workload. A cutoff above one machine word also regressed 64- and 256-byte payloads, leading to the current experimental 1 KiB cutoff. I examined Tokio's block queue and Crossbeam's stamped-slot queue as further design options, but did not implement those queue replacements.

Baseline: merged commit cf7ec96, with identical benchmark fixtures in both builds. Linux x86-64, AMD EPYC-Rome VM with 16 CPUs, rustc 1.99.1 / LLVM 23.1.1, optimized bench builds. One warmup per variant/filter, then eight fresh-process samples per variant/case in alternating AB/BA order, fixed rounds, --baseline-only --repeat 1 --raw. Builds/tests did not run concurrently with measurement. The final matrix contains 17 cases and 272 observations.

Each cell is median ms (min–max); lower is better. Percentage changes are elapsed time, not throughput.

Scenario Baseline Experiment Time change
tasks-4p1c-cap64-4w-inline1k 562 (403–655) 365.5 (148–395) -35.0%
payload-sweep-4096b-tasks 1387 (1238–1527) 781.5 (553–827) -43.7%
tasks-4p1c-cap64-4w 79.5 (76–81) 80 (67–85) +0.6%
ready-roundtrip-inline1k 223.5 (218–235) 240.5 (233–259) +7.6%
ready-roundtrip-boxed1k 68 (67–72) 71.5 (70–74) +5.1%
try-roundtrip 89 (87–91) 89 (87–93) 0.0%

The contended 1 KiB and 4 KiB cases use four producers, one consumer, capacity 64, four Tokio workers, and 26 rounds. Ready cases use 1,000,000 rounds; try-roundtrip uses 3,000,000. The remaining 16/64/256-byte controls had overlapping baseline/candidate ranges; their modest median changes should not be read as established improvements.

There are material tradeoffs: backing-buffer storage doubles for payloads >= 1 KiB (capacity 64 × 1 KiB goes from 64 KiB to 128 KiB, excluding metadata), the 1 KiB ready path regresses 7.6%, and all channels currently gain metadata and an additional counter allocation. Channel-construction cost was not benchmarked. The cutoff was selected on this VM; it needs independent hardware validation, especially macOS/ARM, plus large-message capacity/producer-count sweeps. These observations do not establish the precise cause of the timing changes or resolve #401.

A separate 10-run ecosystem comparison for the 1 KiB contended case measured Asyncband at 367 ms (346–419), Tokio at 226 ms (192–256), AsyncChannel at 235 ms (196–282), and Flume at 666 ms (535–785). Asyncband still takes about 1.62× Tokio's elapsed time here.

Validation passed: 44 MPSC integration tests, 5 public trait tests, Miri (41 passed, 3 OS-runtime-dependent tests skipped), and cargo x lint. Tests include capacity/reservation accounting, partial-drain ordering, reentrant destruction, the 1023/1024-byte boundary, and a capacity-return/waiter-registration race. The prototype adds no unsafe code or runtime dependency. It remains local on experiment/mpsc-large-batches at 651e619; no follow-up PR has been opened.

All final raw samples, in run order within each variant (ms)
payload-sweep-16b-ready / baseline: [44, 45, 47, 45, 44, 46, 45, 47]
payload-sweep-16b-ready / large: [43, 52, 43, 45, 45, 45, 45, 45]
payload-sweep-16b-tasks / baseline: [101, 99, 96, 102, 97, 99, 95, 98]
payload-sweep-16b-tasks / large: [88, 94, 84, 96, 94, 98, 92, 99]
payload-sweep-64b-ready / baseline: [68, 64, 64, 74, 65, 64, 69, 71]
payload-sweep-64b-ready / large: [65, 72, 65, 64, 64, 64, 63, 64]
payload-sweep-64b-tasks / baseline: [190, 178, 176, 179, 167, 183, 170, 167]
payload-sweep-64b-tasks / large: [181, 183, 163, 133, 200, 184, 186, 170]
payload-sweep-256b-ready / baseline: [123, 111, 112, 118, 117, 111, 115, 111]
payload-sweep-256b-ready / large: [113, 121, 111, 115, 132, 114, 116, 115]
payload-sweep-256b-tasks / baseline: [278, 286, 256, 259, 281, 274, 271, 276]
payload-sweep-256b-tasks / large: [233, 284, 326, 222, 304, 229, 289, 316]
payload-sweep-4096b-ready / baseline: [779, 768, 781, 763, 774, 789, 777, 795]
payload-sweep-4096b-ready / large: [791, 784, 802, 797, 791, 790, 771, 784]
payload-sweep-4096b-tasks / baseline: [1435, 1246, 1527, 1365, 1238, 1409, 1332, 1482]
payload-sweep-4096b-tasks / large: [808, 827, 553, 771, 743, 792, 811, 756]
tasks-4p1c-cap64-4w / baseline: [81, 81, 76, 77, 81, 79, 78, 80]
tasks-4p1c-cap64-4w / large: [82, 78, 67, 79, 82, 72, 85, 81]
tasks-4p1c-cap64-4w-inline1k / baseline: [552, 655, 539, 536, 629, 403, 643, 572]
tasks-4p1c-cap64-4w-inline1k / large: [329, 366, 148, 365, 366, 336, 395, 395]
tasks-4p1c-cap64-4w-boxed1k / baseline: [437, 223, 508, 496, 525, 511, 459, 532]
tasks-4p1c-cap64-4w-boxed1k / large: [433, 494, 470, 495, 545, 323, 463, 532]
ready-roundtrip / baseline: [36, 36, 36, 36, 36, 36, 36, 36]
ready-roundtrip / large: [36, 40, 40, 36, 36, 36, 36, 36]
ready-roundtrip-inline1k / baseline: [235, 224, 226, 218, 219, 223, 221, 226]
ready-roundtrip-inline1k / large: [240, 238, 241, 244, 233, 259, 240, 247]
ready-roundtrip-boxed1k / baseline: [67, 67, 68, 67, 70, 70, 72, 68]
ready-roundtrip-boxed1k / large: [72, 71, 70, 71, 70, 73, 74, 73]
try-roundtrip / baseline: [89, 89, 87, 89, 89, 89, 89, 91]
try-roundtrip / large: [89, 87, 90, 89, 88, 89, 90, 93]
tasks-4p1c-cap1-4w / baseline: [1465, 1500, 1459, 1499, 1447, 1506, 1471, 1503]
tasks-4p1c-cap1-4w / large: [1478, 1492, 1505, 1556, 1585, 1475, 1500, 1563]
tasks-1p1c-cap1024-4w / baseline: [787, 957, 1019, 1091, 1004, 1013, 1210, 964]
tasks-1p1c-cap1024-4w / large: [1091, 1086, 974, 1077, 1216, 889, 922, 563]

The additional task controls use 160 rounds for capacity one and 430 rounds for one producer/capacity 1024. Payload-sweep ready/task cases use 1,000,000/26 rounds respectively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants