When one thread needs to tell another that a packet arrived or that its time slice is up, it must either constantly poll a memory flag — which burns whole CPU cores doing nothing — or fire an interrupt, which forces the receiving core to throw away hundreds of in-flight instructions in a pipeline flush; this paper from Purdue, UC San Diego, and collaborators proposes xUI, four small processor changes led by tracked interrupts that deliver a notification without the flush, giving polling's speed with an interrupt's efficiency.
The argument in one glance
-
Problem
Telling one thread something, fast and cheaply, has no good option
Software constantly needs one thread to notify another: a packet has arrived and needs handling, a running thread has used its time slice and should yield, a timer has fired. How that notification is delivered decides whether a system is fast, efficient, and responsive. The two standard ways trade these against each other — one is fast but wasteful, the other efficient but slow — and there is no mechanism that is all three at once.
-
What people did
Poll a shared-memory flag, or send an interrupt
The fast way is polling: repeatedly read a flag in shared memory and act when it flips. The efficient way is an interrupt: the sender pokes the receiving core, which drops what it is doing and runs a handler. Intel recently added user interrupts, letting one thread interrupt another directly without going through the operating system.
-
Why it fell short
Polling burns cores; interrupts flush the pipeline
Polling is cheap per check but its cost scales with how often you check, so it wastes huge numbers of cycles — even whole cores — looking when nothing has happened. An interrupt avoids that, but delivering it forces the receiving core to flush its pipeline, throwing away hundreds of in-flight instructions. That flush keeps interrupts roughly six to nine times slower than polling, and it is getting worse as processors keep more instructions in flight.
-
This paper's idea
Deliver the interrupt without flushing, and cover the rest
xUI is four small processor changes. The central one, tracked interrupts, exploits the fact that an interrupt is asynchronous — the exact instruction it lands on does not matter — to inject the handler at the end of the in-flight stream instead of flushing everything, cutting the receiver cost several-fold. Three more add a precise compiler-marked safepoint, a user-level per-core timer, and a way to route device interrupts to threads — together giving polling's speed with an interrupt's efficiency.
Why this problem is worth solving
The operation at issue is one of the most basic in systems software, which is why shaving its cost pays off everywhere. User-level notification is the act of one thread signalling another that something has happened, done entirely in user space without trapping into the operating system on the critical path. It is the primitive underneath preemptive scheduling in a language runtime, where a scheduler must interrupt a running thread to give another a turn; underneath input-output handling, where a network card or accelerator finishing its work must wake the thread waiting on it; and underneath timeouts and periodic tasks. Because it happens constantly and on the fast path, its per-event cost is multiplied across an entire system, so a mechanism that is a few hundred cycles too expensive quietly taxes throughput, tail latency, and energy across the whole machine. Making notification both fast and efficient is therefore not a niche optimization but a lever on how well modern servers use their cores.
The reason interrupts stay expensive comes down to one specific thing a processor does when one arrives. A pipeline flush is the act of discarding all the instructions a processor has in flight — fetched, decoded, and executing but not yet completed — and starting over. A modern core keeps hundreds of instructions in flight at once to stay busy, speculating ahead past branches. When an interrupt arrives, the conventional response is to flush every one of those in-flight instructions so the handler can run immediately on a clean pipeline. That minimizes the delay before the handler starts, but it throws away all the useful work those instructions represented, and refilling the empty pipeline afterward takes time too. Measurements in the paper show this flush-and-refill is the single largest chunk of an interrupt’s cost — over 400 of the roughly 700 receiver cycles. Worse, as future processors hold ever more instructions in flight, the flush throws away ever more work, so the gap between interrupts and polling is set to widen rather than close.
What people did before, and where it broke
- Poll a shared-memory flag. Have the receiver repeatedly read a variable and act when the sender flips it — the fastest known notification, and precise, since the code controls exactly where checks happen. Where it broke. The cost scales with the number of checks, not the number of events, so it wastes cycles whenever nothing is pending; adding preemption checks to loops slowed programs by up to 96 percent in one runtime, and polling for input-output can burn entire cores spinning on queues that are idle.
- Dedicate a core as a timer and notify the others. Because user runtimes have no cheap access to hardware timers, they set aside a whole core to spin on a high-precision clock and send notifications when threads need preempting. Where it broke. That is a whole core spent on timekeeping, and the cost of notifying the others grows with the number of cores, so the approach scales badly and wastes hardware.
- Use operating-system signals. Deliver notifications through the kernel signal machinery. Where it broke. Signals are expensive — thousands of cycles per delivery because of the kernel round trip — and imprecise, arriving at an arbitrary instruction, which does not suit runtimes that need to be interrupted only at safe points.
- Use Intel user interrupts (UIPI). The recent hardware feature that lets one thread interrupt another directly, without the kernel, at three to five times lower cost than a signal. Where it broke. It still flushes the receiver pipeline, leaving it six to nine times slower than polling; it is imprecise, so it clashes with precise garbage collection; and it is narrow, handling only thread-to-thread interrupts, not timers or device interrupts.
- The gap. Every option gave up at least one of speed, efficiency, precision, and generality. Polling is fast and precise but wasteful; interrupts are efficient but slow and imprecise; user interrupts narrowed the gap but did not close it and covered only one case. No mechanism delivered a notification with the speed of polling, the efficiency of an interrupt, the precision of a marked safepoint, and coverage of timers and devices.
How xUI is put together
The four pieces of xUI, over the user-interrupt baseline
-
-
new
-
new
-
new
-
new
-
new
The idea
An interrupt is asynchronous, so there is no need to flush the work in flight. The whole expense of an interrupt comes from treating the instruction it happens to land on as a meaningful boundary: the processor flushes everything after that point so the handler can run on a clean pipeline. But an interrupt is not synchronous — nothing about the program says the handler must run precisely between two particular instructions; it only has to run soon. Tracked interrupts exploit exactly this freedom. Instead of flushing, the processor injects the handler micro-ops at the end of the in-flight instruction stream and lets the existing work keep going, tracking the injected instructions through the pipeline so they are re-issued if a branch misprediction later flushes them, and remembering the right address to return to. Because the injected handler instructions do not depend on any of the in-flight program instructions, they enter execution immediately, so this gets the no-wasted-work property of letting the pipeline drain without paying the latency of waiting for it. The receiver cost falls from around 700 cycles to 231, or to 105 for a timer or device interrupt that skips the extra bookkeeping. Recognizing that a constraint everyone treated as a hard synchronization point is really an artifact — the interrupt does not care where it lands — and dissolving it to avoid a costly stall is the same move the non-blocking automata-processing work covered elsewhere on this site makes when it finds that the barrier serializing the computation belonged to the data structure, not to the problem, and removes it.
Give user code direct hardware access so the operating system leaves the fast path. Two of the four pieces are about cutting the kernel and its proxies out of notification entirely. A user runtime that wants microsecond-scale timing today has no cheap way to reach a hardware timer, so it dedicates a whole core to spinning on a clock and pays kernel overhead to notify the others; the KB_Timer instead gives every core its own timer that user code sets and clears with two new instructions, delivering an event in 105 cycles and freeing that dedicated core — saving one core for roughly every twenty-two workers in the paper measurements. Likewise, a device interrupt normally lands on the operating system, which is why kernel-bypass input-output frameworks make user code poll instead; interrupt forwarding extends the interrupt controller to route a device interrupt straight to the thread running on the target core, so the thread is woken directly. In both, the expensive privileged-boundary crossing that made the efficient path slow is removed, and user space gets the hardware access it needed. Taking a mechanism whose safe version was slow because it went through a privileged boundary, and making it fast by giving user code a direct, cheap path instead, is the same bargain the exit-less virtual-machine work covered elsewhere on this site strikes, replacing the costly trap out of the guest with a lightweight switch so the protection stops dominating the cost.
Let the compiler tell the hardware exactly where it is safe to interrupt. Precise preemption — stopping a thread only at points where the runtime knows its state is consistent, as garbage collection requires — has been a software burden, implemented by having the compiler insert polling checks that constantly test a flag and cost real time in hot loops. Hardware safepoints move that contract into the hardware: the compiler marks the safe points, cheaply, by tagging instructions with a prefix so any instruction can serve as one, and the processor, in safepoint mode, simply delays delivering a pending interrupt until it reaches one. When no interrupt is pending a safepoint is just a normal instruction, so the common case is free, and preemption becomes precise without the constant polling tax — a slowdown of about one percent against eight to eleven percent for compiler-inserted polling. Having the compiler and the hardware cooperate through a small, agreed marker, so that a job software used to do laboriously by hand becomes a near-free hardware primitive, is the same division of labour the paging-replacement work covered elsewhere on this site is built on, where the compiler and the privileged layer cooperate to retire a costly mechanism rather than each paying for it alone.
Following a tracked interrupt through the pipeline
Delivering an interrupt without throwing away in-flight work
A notification arrives at a receiver core that is busy with hundreds of speculated instructions. This traces tracking, contrasted with the flush that a conventional user interrupt would perform.
Step 1 of 5
The interrupt arrives mid-flight
The receiving core has, say, three hundred instructions in flight, fetched and executing past several branches. A conventional user interrupt would flush all of them now, so the handler starts on an empty pipeline — throwing away every bit of that work.
That flush and the refill afterward account for over 400 of the roughly 700 cycles a user interrupt costs the receiver, and the waste grows as pipelines get deeper.
Why it works
- Asynchrony means the handler can be injected, not forced in. Flushing treats the interrupt point as a synchronization boundary and discards everything after it → but an interrupt only has to run soon, so its micro-ops can be appended to the in-flight stream instead → which removes the 400-plus cycles of flush and refill, dropping the receiver cost to 231 or 105 cycles.
- Independent handler instructions execute without waiting. Draining the pipeline avoids wasted work but stalls until it empties → but the injected handler does not depend on the in-flight program instructions, so it runs immediately even with a full window → giving the no-waste benefit of draining with the low latency of flushing, and neither downside.
- A per-core user timer removes a whole class of overhead. Reaching a timer through the operating system or a dedicated timekeeping core is slow and scales poorly with core count → but a KB_Timer gives each core its own timer set directly from user space → so a 105-cycle timer event needs no kernel and no dedicated core, saving roughly one core per twenty-two workers.
- Compiler-marked safepoints make precise preemption nearly free. Precise preemption via inserted polling checks taxes hot loops constantly → but marking safe points with a prefix and having hardware wait for one costs nothing when no interrupt is pending → so preemption stays precise at about a one-percent slowdown against eight to eleven percent for polling, and precise garbage collection works.
- Forwarding device interrupts to threads ends the spin. Kernel-bypass input-output forces user threads to poll because device interrupts land on the kernel → but forwarding routes the interrupt straight to the running thread → so the thread is woken directly and stops burning cycles polling, freeing 45 to 100 percent of a core that spinning consumed, at latency within a fraction of a microsecond of polling.
What it costs (trade-offs and scope)
- It is evaluated in simulation, not on a real chip. All four mechanisms are microarchitecture proposals modeled in the gem5 simulator; the authors carefully calibrated their user-interrupt costs against a real Sapphire Rapids processor, but the behavior of tracked interrupts inside a full out-of-order pipeline is modeled, not measured on silicon.
- They had to fix the simulator to get a trustworthy baseline. gem5’s own interrupt model was wrong — it drained the pipeline and added a made-up 13 cycles — so the authors rebuilt it to match real hardware. The results are sound only to the extent that corrected model is faithful, and it rests on their reverse-engineering of an undocumented feature.
- Tracked interrupts have an ugly worst case. When a handler micro-op depends on a long chain of cache-missing loads already in flight, delivery can stall close to 7000 cycles, an order of magnitude worse than a flush. The authors call this pathological and rare, and note they are at the limit of what reverse-engineering and simulation can confirm.
- The input-output latency is not always better than polling. For the layer-3 router, tail latency was within a few percent of polling for one and four network cards but 65 percent worse for eight — the consistent win is efficiency, the freed cycles, not universally lower latency.
- Interrupt forwarding is cramped by the vector space. A core has only 256 interrupt vectors, shared among all its threads, which limits how many device-and-thread pairs forwarding can support at once; scaling further would need changes to the interrupt message format.
- It requires new instruction-set and microarchitecture support. Tracked interrupts, the KB_Timer instructions, the safepoint prefix, and the interrupt-controller extensions all have to be built into a processor, so nothing here runs on today’s hardware — it is a proposal to chip vendors, however small its claimed bill of materials.
- The gains are workload-dependent. Freeing the timer core helps from about 4.5 percent to a doubling of throughput depending on how many cores are in play; the RocksDB throughput gain over user interrupts was 10 percent; the safepoint benefit was shown by modifying one specific compiler-instrumentation system. The headline numbers are representative, not guarantees.
- The precise-garbage-collection story is sketched, not built end to end. Safepoints are shown to be cheap and the compiler side is described as a small change, but the evaluation adapts an existing instrumentation pass rather than integrating with a full managed-language garbage collector.
The evaluation
Four ways to deliver a user-level notification
| Property | Polling a memory flag | Dedicated timer core + signals | Intel user interrupts (UIPI) | xUI |
|---|---|---|---|---|
| Receiver cost per notification | ||||
| Wastes cycles when nothing is pending | ||||
| Precise (safe for garbage collection) | ||||
| Covers timers and device interrupts | ||||
| Result | Fast and precise, but burns cores | Expensive and complex | 6 to 9 times slower than polling, narrow |
Check yourself
Check your understanding
1. Why do the two standard ways to notify a thread each fall short?
2. What is the key insight behind tracked interrupts?
3. How do hardware safepoints make precise preemption nearly free?
4. Which limitation most affects how xUI's results should be read?
Sources
The paper is “Extended User Interrupts (xUI): Fast and Flexible Notification without Polling” by Berk Aydogmus, Linsong Guo, Danial Zuberi, Tal Garfinkel, Dean Tullsen, Amy Ousterhout, and Kazem Taram, published at ASPLOS 2025, pages 373-389, DOI 10.1145/3676641.3716028. The authors are at Purdue University and the University of California, San Diego.
This explainer is written from the full 17-page text. Everything above comes from the paper’s own sections: the introduction and the limitations of user-level notification, quantifying the costs of polling (including runtime slowdowns up to 96 percent), dedicated timer cores, signals, and Intel user interrupts; the characterization of Intel UIPI, the first reverse-engineering of its microarchitecture, including the UPID and UITT data structures, the send and receive paths, the measured costs (a 383-cycle send, a 720-cycle receiver overhead, a 1360-cycle end-to-end latency), and the deconstruction identifying the pipeline flush as the dominant cost; the xUI design section presenting tracked interrupts and their front-end tracking mechanism, the hardware safepoints implemented via an instruction prefix and an instruction-boundary check, the KB_Timer with its set and clear instructions and per-core physical timer, and interrupt forwarding via extensions to the local interrupt controller; and the experimental methodology and evaluation on gem5, including the correction of gem5’s interrupt model, the receiver-overhead reductions (645 to 231 to 105 cycles, a 6.9-times overhead reduction), the safepoint-versus-polling comparison, the timer-core savings, the worst-case latency analysis, and the three end-to-end use cases — RocksDB preemption in the Aspen runtime, DPDK layer-3 forwarding, and a simulated data-streaming accelerator.