Re: [PATCH] io_uring/napi: drop the single NAPI busy poll special case
From: Cinq Prairie
Date: Mon Oct 26 2026 - 19:17:57 EST
On Mon, 26 Oct 2026 07:46:38 -0600, Jens Axboe wrote:
> Does dropping the single-NAPI path trade the early need_resched() exit
> for a full busy poll start/stop cycle on every poll?
Yes, and without prefer_busy_poll that is a regression. With loop_end
NULL every lap ends in busy_poll_stop(), which runs the driver poll a
second time; napi_complete_done() then clears NAPIF_STATE_SCHED and the
driver unmasks the device interrupt. The next packet raises it, the
handler calls napi_schedule() and sets SCHED again. As a result the next
__napi_busy_loop() lap on the polling CPU sees SCHED set and skips
napi_poll(). The hardware descriptor ring ends up alternating between
busy-poll and softirq instead of being held by the poller. My numbers
only covered the early exit case with prefer_busy_poll on, so this did
not show. Agreed that dropping the fast path isn't acceptable for
stable.
> but io_napi_busy_loop_should_end() is io_uring's own callback, so could
> it check an absolute deadline stored in iowq instead of the start_time
> that net core passes in?
Yes, that works and is way cleaner than either alternative I explored!
What I have in mind for v2: store an absolute deadline in iowq and keep
the singular fast path but drop the "&& !loop_end_arg" so the loop
re-enters for the remaining budget after a need_resched() exit.
> This isn't a bug, but with the special case gone neither the single nor
> the multi NAPI path checks need_resched() at all for the whole budget,
> and the only preemption point is the preempt_enable() between polls
> inside __napi_busy_loop(). On a kernel without preemption that is up to
> 10 ms with no cond_resched(). Should the message mention that the
> single-NAPI case now stops yielding to a woken task on those
> configurations?
I'm thinking that with the re-entry in place io_uring can cond_resched()
when the napi loop bailed on a need_resched() then resume, between laps
it holds nothing from the list and is in sleepable context. That would
mirror the socket path. Though it also impacts the multi-NAPI path,
which never yielded so far, so I'd like your preference: include it in
v2, or keep v2 to the single-NAPI fix and send the yield as a follow-up?
I'll also add numbers for the single-NAPI fast path when it is not cut
short, before/after, so the cost is visible.