Zero-copy I/O and ownership
Evaluate copy avoidance as an ownership tradeoff, with explicit reuse notifications, retained-memory budgets, and measurement boundaries.
On this page
The question for today
What you’ll understand
- Explain why copy avoidance can lengthen application buffer lifetimes.
- Distinguish accepted-byte results, buffer-reuse notifications, and peer-level success.
- Estimate retained memory and an illustrative copy-avoidance break-even point.
Copying a payload gives the receiver of the copy an independent version of its bytes. Avoiding that copy removes work but preserves a relationship between the producer’s memory and a later consumer. In asynchronous networking, the operating system may still reference application pages after the send call returns or after an initial result arrives. Zero-copy engineering therefore starts with ownership and evidence, then asks whether the reduced copying outweighs page accounting, notification processing, and extra retained memory. The name alone promises neither faster requests nor successful delivery.
Replace a copy with a temporary access obligation
Consider an immutable payload built in an application buffer. With a copying send path, the kernel can often retain its own copy after accepting bytes. With copy avoidance, the application must preserve the relevant region until the API supplies the reuse signal. Keep the allocation alive and prevent writes through aliases, including slices handed to another task. A memory-safe language can still expose a logically unsafe design if an unsafe I/O boundary or external buffer pool ignores these access obligations.
Linux MSG_ZEROCOPY provides a notification when the shared storage can be reused; asking for copy avoidance does not guarantee that copying is avoided. Its notification is not proof that bytes reached the peer, and deferred copying can release storage before transmission finishes. Treat notification processing as part of the send path, not optional housekeeping. Registered or pinned pages are likewise not magically immutable: registration optimizes resource setup, while your ownership protocol must prevent accidental overwrites.
Separate the result from permission to reuse memory
For io_uring zero-copy sends, inspect the initial completion flags. If that result carries IORING_CQE_F_MORE, the operation promises a later notification; IORING_CQE_F_NOTIF identifies that notification. Keep the required memory unchanged until the appropriate notification arrives. Do not unconditionally wait for a second entry when the initial result does not promise one. Maintain an operation record capable of handling both events, errors, and their documented ordering without returning the same allocation to its pool twice.
The initial result describes accepted bytes or an error, so partial sends still require explicit handling of the unsent region. Buffer reuse, local transmission, transport acknowledgement, and application acknowledgement answer different questions. If your product requires durable peer storage, use a peer protocol that confirms that condition. Cancellation cannot replace this evidence: a request to cancel a send is neither a promise that the device stopped nor permission to overwrite pages that remain referenced.
Measure total cost and make retention bounded
A useful benchmark measures more than payload throughput. Include CPU time, copied-byte fallback rate, notification lag, peak held bytes, and application latency across payload sizes. Keep the workload’s network path representative; a local loopback test can exercise different behavior from an actual network device. Compare total work at equal correctness guarantees. Dropping notifications to improve a benchmark merely removes the mechanism needed to reclaim memory, producing a misleading result and eventually exhausting the pool.
Try the queue lab with arrivals interpreted as payload buffers becoming retained and service as reuse notifications processed. Press Reset, set both arrival and service rates to 3, predict whether the initial four-unit backlog drains, then press Play. For a growth comparison, press Reset again, explicitly set arrival rate to 4 and service rate to 3, predict the new slope, then press Play. A separate Reset with both rates set to 2 and 3 permits a draining comparison; predict before pressing Play. Each experiment starts afresh at four units. This comparison motivates a bounded pool and admission control, but the lab has no page pinning, packet sizes, partial sends, or notification batching. Its stable M/M/1 readout is a separate idealized queue estimate, not a measured network or memory-release latency.
Work it through
Budget retained payload memory
A stable service sends equal 64 KiB payloads at 10,000 sends per second. The average interval from submission to reusable-buffer notification is 4 ms. Estimate average retained payload memory, then repeat for 8 ms.
Convert the holding interval to seconds: 4 ms = 0.004 s. The mean number of retained payloads is 10,000 × 0.004 = 40, using the same submission-to-reuse boundary throughout.
Multiply 40 by 64 KiB to obtain 2,560 KiB, or 2.5 MiB, of average retained payload storage. This excludes metadata, allocator slack, and any preallocated free buffers.
At 8 ms the average retained count becomes 80 and payload memory becomes 5 MiB. Throughput stayed the same; a longer release interval doubled the required average ownership footprint.
Copy avoidance retains an average of 2.5 MiB in the first scenario and 5 MiB in the second. Neither mean is a safe peak-capacity bound; inspect bursts and notification-delay tails separately.
The living notebook
Explore the model
Interactive lab · queue
Delayed notifications retain buffers
Press Reset, set both arrival rate λ and service rate μ to 3, predict whether the initial four units drain, then press Play. For the overloaded comparison, press Reset again, explicitly set λ = 4 and μ = 3, predict growth, then press Play. Each is a fresh fluid run, not a live rate switch.
- Fluid backlog
- 4
- Net rate · λ − μ
- −1 /s
- M/M/1 mean residence
- 1 s
Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forEqual rates preserve the initial backlog in this deterministic model. Real zero-copy buffer release requires operation-specific notifications; this lab only illustrates retention pressure.
Pause & reason
Check your understanding
Your turn
Try it yourself
In the illustrative CPU-cost model, copying bandwidth is 20 GB/s using decimal units, and copy-avoidance overhead is 3 microseconds per payload. Find the break-even payload size. Estimate both costs for a 1 MB payload, and name one reason a real benchmark could disagree.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Multiply bytes per second by overhead in seconds. Use 1 MB = 1,000,000 bytes for the second calculation.
Compare with the solution
The threshold is 20,000,000,000 × 0.000003 = 60,000 bytes. Copying 1,000,000 bytes costs about 50 microseconds in this model; the fixed avoidance cost is 3 microseconds, suggesting 47 microseconds less CPU work. Actual page accounting, cache behavior, fallback copies, notification handling, or contention can overturn that estimate. The calculation does not predict end-to-end network latency.
Keep the idea
Take it with you
- Copy avoidance trades byte copying for a longer, explicitly tracked access lifetime.
- Accepted bytes, reusable memory, and peer-level success require different evidence.
- Benchmark total cost and keep retained storage bounded under delayed notifications.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.