tschneidereit opened PR #13840 from tschneidereit:pooling-perf to bytecodealliance:main:
This changes the pooling allocator to use a shard per thread (capped at 16) of all relevant pools.
With this, lock contention in the pooling allocator is completely eliminated in local testing with a Rust Hello World WASIp2 HTTP proxy component.performance of
wasmtime serve -S cli hello.wasmgoes from 135k RPS to about 168k RPS.
tschneidereit added the label wasmtime:pooling-allocator on PR #13840.
tschneidereit requested fitzgen for a review on PR #13840.
tschneidereit requested wasmtime-core-reviewers for a review on PR #13840.
tschneidereit added the label performance on PR #13840.
tschneidereit updated PR #13840.
tschneidereit updated PR #13840.
:memo: fitzgen submitted PR review:
Great idea! A handful of comments below
:speech_balloon: fitzgen created PR review comment:
Can we define a
ShardIdnewtype? And then have helpers on each pool type to go fromShardIdto its sharded pool.This would also let
ShardIdbe represented as au32, which should make some of the data structures here quite a bit more compact (eg theper_shardinfree_many).
:speech_balloon: fitzgen created PR review comment:
This logic to distribute slots across shards is a little subtle, especially in the face of a pool whose capacity doesn't evenly divide by the shard size. Could we have a
proptestormutatis::checktest that asserts that summing up each shards slots and max unused warm slots gives us the expected total? Might want to factor out some pure logic while doing this so that theavailable_parallelismcall becomes a parameter and the property test isn't checking only the host's hardware configuration.
:speech_balloon: fitzgen created PR review comment:
This logic seems to be repeated a few times as well, could we wrap it up in a helper?
:speech_balloon: fitzgen created PR review comment:
Should
flush_all_decommit_queuesinstead be something likeflush_next_decommit_queuewhere it returnstrueas soon as any sharded queue's decommit returned a slot to the pool, rather than eagerly flushing all slots? This would avoid a number of lock acquisitions (at the cost of raising the chances that someone else steals the flushed slots before we get a chance to grab it). A good balance might be something like this:let mut error = None; assert!(self.decommit_queues.len() > 0); for _ in 0..self.decommit_queues.len() { self.flush_next_decommit_queue() { match f() { Ok(x) => return Ok(x), Err(e) => { error = Some(e); continue; } } } } Err(error.unwrap())
:speech_balloon: fitzgen created PR review comment:
FWIW, I'm imagining something similar to our existing property test here: https://github.com/bytecodealliance/wasmtime/blob/6918520e213c1cce4c4f790907d2f9482056e3ed/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs#L1015
github-actions[bot] added the label wasmtime:api on PR #13840.
tschneidereit updated PR #13840.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Agreed yeah, and I wasn't too comfortable with the subtlety here either. I hadn't thought of using a proptest though, which I think is a great idea.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Good call, yeah, and that's roughly what I implemented now. We could alternatively introduce something like
free_many_and_alloc_onewhich would remove the risk of the slot being stolen. I'd like to at least do that in another PR though, given that the issue is preexisting, to the degree it's a real concern.
tschneidereit requested fitzgen for a review on PR #13840.
:thumbs_up: fitzgen submitted PR review:
LGTM, thanks!
fitzgen added PR #13840 Shard pools for memories, tables, and stacks to reduce/eliminate lock contention to the merge queue.
github-merge-queue[bot] removed PR #13840 Shard pools for memories, tables, and stacks to reduce/eliminate lock contention from the merge queue.
tschneidereit requested alexcrichton for a review on PR #13840.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Could this method be deleted and tests updated to call
deallocate_many?
:speech_balloon: alexcrichton created PR review comment:
Did you see much benefit from using this? I'd naively expect this to be a bit too low level of a micro-optimization for us to see much benefit in the pooling allocator, but I'm also not entirely sure.
:speech_balloon: alexcrichton created PR review comment:
FWIW if you drop the
constthis'll get evaluated at first-use meaning that the initialiation below with afetch_addcan be placed here directly and then it can be accessed as-is.
:speech_balloon: alexcrichton created PR review comment:
Can this update the documentation of
decommit_batch_sizeinconfig.rsto indicate that the queued-to-be-released can actually be up to16 * (batch_size - 1)as opposed tobatch_size - 1?
:speech_balloon: alexcrichton created PR review comment:
I realize this is preexisting, but one possible improvement here would be to drop the
queuelock before the closuref()is called to minimize the scope of the critical section here. Same for memory/table allocation below, too.
:speech_balloon: alexcrichton created PR review comment:
I think this allocation is one reason CI is failing, notably this isn't handling OOM. We've got
try_collecthelpers though which can then be used to handle this
:speech_balloon: alexcrichton created PR review comment:
Huh well ok given that preexisting this was
.collect()maybe I'm totally off about what's causing the failure in fuzzing....
:speech_balloon: alexcrichton created PR review comment:
This
.collect()I think may also need to becometry_collectto handle OOM
:speech_balloon: alexcrichton created PR review comment:
I think this is failing on miri since this reads the local current directory, but feel free to put
#[cfg_attr(miri, ignore)]on these proptest tests
:speech_balloon: alexcrichton created PR review comment:
This I think is the same as
decommit_shard_idsin the main pooling allocator, could there perhaps be a helper that returns animpl Iteratorof shard ids starting with the current thread's home and wraps around?
:speech_balloon: alexcrichton created PR review comment:
A few thoughts on this: primarily could this be deduplicated with the above method? We've gotten ids/shards/etc wrong here in the past so it's subtle enough I'd prefer not to duplicate.
Otherwise though, could/should this use a
SmallVecin the same manner as the index allocator? Looking at this again, too, this might be where the OOM tests are failing (OOM on thepushhere). We don't have an OOM-handling version ofSmallVec, however, meaning that switching toSmallVecwould perhaps paper over the OOM happening without actually addressing it, but that could be handled in a follow-up
tschneidereit updated PR #13840.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done — removed, and the test now goes through
deallocate_many.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done — the thread-local now initializes straight from the
fetch_add, no sentinel orCellneeded.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
I didn't A/B this specifically, so I can't claim a measured win for it. The rationale: the shards are stored inline in the
Box<[_]>, so without padding the tail of one shard'sInner/DecommitQueue(frequently-written fields) can end up on the same cache line as the next shard's mutex word. The cost is bounded (≤16 shards × ≤128 bytes of padding per pool), so I kept it as cheap insurance — but happy to drop it if you'd rather not carry it without a measurement.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
That's actually already the case, though it's easy to miss:
flush_decommit_queuetakes the queue out of the mutex and drops the guard before doing the actual decommit, so neither the flush nor the retry off()(nor the memory/table allocation retries below) runs while a queue lock is held. I extended the doc comment onwith_flush_and_retryto make that explicit.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done — this now uses
try_collect, and the index-allocator constructors are fallible as well. FWIW, the abort in the OOM tests turned out to bepooling_allocator_fiber_stack_slot_leak_on_oomhitting the intermediateVecthatStackPool::deallocate_many/TablePool::deallocate_manybuilt on the deallocation path; those now feed their iterator straight intofree_manyinstead. The--cfg arc_try_newOOM suite aborted before these changes and passes with them (188/188 locally).
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done —
ShardLayout::newand the allocator constructors now returnResult<_, OutOfMemory>and usetry_collect.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
The two fuzz-test failures in the queue run (
smoke_test_memory_accessandgc_ops_eventually_gcs) do look unrelated to this PR: they failed on the GC heap's minimum size exceeding the configured memory limit, i.e. an interaction with the state main was in during that merge-queue batch (#13841 was in flight at the time). Both tests pass locally on this branch merged with current main. The OOM-test abort was real, though — see the other thread.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done — added a
shard_ids_from_homehelper in pooling.rs, used by both the decommit-queue paths and the index allocator'sallocprobing.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
Done on both counts: the subtle slot/stripe translation now lives in a shared
return_slothelper used by both methods, anddeallocate_manybatches per-stripe frees inSmallVecs of the same shape as the index allocator'sfree_many. The OOM-on-spill caveat applies as you say and would be for a follow-up — the abort the OOM tests actually hit was the stack/table pools' intermediateVecs, which are now gone entirely.
github-actions[bot] added the label wasmtime:config on PR #13840.
tschneidereit updated PR #13840.
tschneidereit commented on PR #13840:
Hold off on re-reviewing: I'm not actually done with this yet and hadn't intended to push for now :/
tschneidereit requested alexcrichton for a review on PR #13840.
tschneidereit commented on PR #13840:
@alexcrichton ok, I think this is ready again now
:speech_balloon: alexcrichton created PR review comment:
Ah ok, it make sense that it helps a bit but I feel like I keep reading various bits and pieces on the internet about how doing this is a bad idea. I think the bad idea part though specifically stems from taking up way too much space for frequently allocated things, which this doesn't fall under, so seems fine to leave
:memo: alexcrichton submitted PR review.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Oh right I remember also missing that aspect the last time I looked at this too, thanks though!
alexcrichton added PR #13840 Shard pools for memories, tables, and stacks to reduce/eliminate lock contention to the merge queue.
:check: alexcrichton merged PR #13840.
alexcrichton removed PR #13840 Shard pools for memories, tables, and stacks to reduce/eliminate lock contention from the merge queue.
Last updated: Jul 29 2026 at 05:03 UTC