Skip to content

Dispatch immediate tasks without a work item - #200

Open
rnro wants to merge 1 commit into
apple:mainfrom
rnro:alloc/run-immediate
Open

rnro wants to merge 1 commit into
apple:mainfrom
rnro:alloc/run-immediate

Conversation

@rnro

@rnro rnro commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

DefaultScheduler.runImmediate wrapped every task in a DispatchWorkItem,
which allocates the work item and copies the task's block a second time. Every
NetworkContext.async goes through it, which includes the hop that delivers
each inbound datagram and the cleanup a stream schedules when it is torn down.
The work item was only there because its initializer accepts a non-Sendable
closure; the task is now handed to the queue directly, just as unchecked. Each
task costs two allocations instead of four.

Measured on my Mac against main with the package's QUIC benchmark tools.
Allocation counts come from full malloc stack logging, which records every
allocation:

QUICTransfer -size 1200, per message      24.37 -> 22.14
QUICTransfer, per 500 KB transfer       6,187.6 -> 6,131.8
QUICHandshake, per connection           1,947.4 -> 1,941.5
QUICStreamLoad, per stream                111.8 -> 106.9

Wall-clock time comes from running main, this change and the other changes
measured alongside it in a rotating order for 9 rounds, and comparing each run
with main's in the same round. Changes moved paths they do not touch by up to
about 1.3%, so differences that size count as noise. The stream load took 1.4%
less time, faster in 7 of 9 rounds; the transfers and the handshakes did not
change beyond noise.

`DefaultScheduler.runImmediate` wrapped every task in a `DispatchWorkItem`,
which allocates the work item and copies the task's block a second time. Every
`NetworkContext.async` goes through it, which includes the hop that delivers
each inbound datagram and the cleanup a stream schedules when it is torn down.
The work item was only there because its initializer accepts a non-`Sendable`
closure; the task is now handed to the queue directly, just as unchecked. Each
task costs two allocations instead of four.

Measured on my Mac against `main` with the package's QUIC benchmark tools.
Allocation counts come from full malloc stack logging, which records every
allocation:

  QUICTransfer -size 1200, per message      24.37 -> 22.14
  QUICTransfer, per 500 KB transfer       6,187.6 -> 6,131.8
  QUICHandshake, per connection           1,947.4 -> 1,941.5
  QUICStreamLoad, per stream                111.8 -> 106.9

Wall-clock time comes from running `main`, this change and the other changes
measured alongside it in a rotating order for 9 rounds, and comparing each run
with `main`'s in the same round. Changes moved paths they do not touch by up to
about 1.3%, so differences that size count as noise. The stream load took 1.4%
less time, faster in 7 of 9 rounds; the transfers and the handshakes did not
change beyond noise.
// Tasks are not `Sendable`; the queue is what serializes them, so the task is handed over
// unchecked. Wrapping it in a `DispatchWorkItem` would skip the check too, but costs a work
// item and a second block copy on every task.
nonisolated(unsafe) let task = task

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Any other way we can do this here without nonisolated?

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