Consider a server delivering static content at 100 Gbps. That is 12.5 gigabytes per second of payload, and on a conventional read-then-write path every byte crosses the memory bus several times before it reaches the wire. A single core's memcpy throughput is typically on the order of 10 GB/s, so the copies alone can consume an entire core. That happens before any protocol processing, encryption, or application logic has run.

The canonical path is instructive. A read() followed by a write() incurs four user-kernel mode transitions and four data copies. First, a DMA transfer moves data from storage into the page cache. Then a CPU copy moves it into a user buffer, a second CPU copy moves it into the socket buffer, and a final DMA transfer sends it to the NIC. Two of those copies serve no computational purpose. The data is not inspected, transformed, or validated. It is merely relocated so that protection boundaries remain intact.

Zero-copy techniques attack this waste at different layers. sendfile and splice keep data inside the kernel. mmap shares pages between kernel and process. DPDK removes the kernel from the data path entirely. Each eliminates a distinct cost, and each introduces a new one. The engineering question is never whether copying is wasteful in the abstract. It is whether copying is the binding constraint for a given workload, and what its elimination costs in exchange.

Sendfile and Splice: Bypassing User Space

sendfile(2), introduced in Linux 2.2, collapses the read-write pair into a single system call. It transfers data from a file descriptor to a socket without ever surfacing that data in user space. Mode transitions drop from four to two, and the copy into a user buffer disappears. In its original form, one CPU copy remained: from the page cache into the socket buffer.

The remaining copy was eliminated once NICs supported scatter-gather DMA and checksum offload. With both features, the kernel appends only descriptors to the socket buffer: page references, offsets, and lengths. The NIC then gathers payload directly from page cache pages. The result is two DMA transfers and zero CPU copies of the payload. The CPU touches headers and metadata, but the payload bytes never pass through its caches on their way out.

splice(2) generalises the idea. It moves data between a file descriptor and a pipe by transferring page references rather than bytes. This enables arbitrary in-kernel pipelines, such as file to socket or socket to socket for proxying, with the pipe serving as a kernel-resident buffer of page pointers. The companion calls tee(2) and vmsplice(2) duplicate references and map user pages into a pipe, respectively. vmsplice carries subtle obligations around buffer reuse that are easy to violate.

Note what this technique bypasses: not the kernel, but user space. The gains are therefore bounded by the cost of the eliminated copies and transitions, and that cost scales with payload size. For a 1 KB response, the system call, TCP processing, and interrupt handling dominate, and removing a 1 KB memcpy is noise. For multi-megabyte objects served from a warm page cache, copy cost dominates, and sendfile can recover substantial CPU and memory bandwidth. The same logic governs Linux's MSG_ZEROCOPY. Its documentation notes it is generally effective only for writes above roughly 10 KB, because page pinning and completion notification carry fixed overhead.

The decisive complication is encryption. If TLS runs in user space, the payload must be read into the process for encryption, and sendfile no longer applies. Kernel TLS (kTLS) restores the path by performing record encryption inside the kernel. NIC TLS offload goes further by encrypting during transmission. Netflix's published work combining sendfile with kTLS on FreeBSD reports per-server throughput in the hundreds of gigabits per second. At that scale, memory bandwidth, not CPU cycles, became the governing resource.

Takeaway

Copy costs scale with bytes, while system call and protocol costs scale with operations, so measure which dominates before optimising either. Avoiding a copy of data that must be encrypted anyway only relocates the work unless encryption moves into the kernel or the NIC.

Memory-Mapped I/O: Trading Copies for Translation

Memory-mapping a file with mmap(2) maps page cache pages directly into the process address space. A subsequent write() to a socket eliminates the page-cache-to-user copy, leaving one CPU copy from the mapped region into the socket buffer. On paper, this is a strict improvement over read-then-write. In practice, mmap replaces an explicit, predictable cost with implicit costs that are harder to see and harder to bound.

The first implicit cost is fault handling. Pages are populated lazily, so first access to each page triggers a fault. If the page is already cached, this is a minor fault. If not, it is a major fault requiring I/O. Minor faults are commonly measured at hundreds of nanoseconds to a few microseconds, while copying 4 KB at 10 to 20 GB/s takes roughly 200 to 400 nanoseconds. The arithmetic is uncomfortable: for a single pass over a fresh mapping, faulting a page in can cost as much as copying it. MAP_POPULATE and madvise(MADV_WILLNEED) amortise this cost but do not eliminate it.

The second is translation overhead. With 4 KB pages, mapping a 1 GB file requires 262,144 page table entries. A modern second-level TLB holds on the order of 1,500 to 2,000 entries, giving a reach of roughly 6 to 8 MB. Random access across a large mapping therefore incurs frequent TLB misses and page walks. Each walk costs tens of cycles, and considerably more when the page table entries themselves miss in cache. Huge pages extend reach by a factor of 512, but file-backed huge page support remains filesystem-dependent and less mature than for anonymous memory.

The third cost appears at teardown and scales with parallelism. Other cores may hold cached translations for pages being unmapped or reprotected. Invalidating them requires a TLB shootdown: inter-processor interrupts instructing every relevant core to flush those entries. On many-core machines, workloads that frequently map and unmap regions can spend more time synchronising translation state than they would have spent copying. Failure handling is also worse. If another process truncates a mapped file, access produces SIGBUS, an error far less tractable than a short read.

Crotty, Leis, and Pavlo documented these hazards in the database context. They argued that mmap's convenience conceals failure modes in eviction control, I/O stalls, and shootdown scalability. The lesson generalises beyond databases. mmap is effective for long-lived mappings accessed repeatedly, where faults and translations amortise over many reads. It is a poor fit for transient, single-pass transfers, which is precisely the case where sendfile is already superior.

Takeaway

A copy is a cost that appears plainly in a profile, while faults, TLB misses, and shootdowns are costs distributed across the system. Prefer the explicit cost unless the access pattern guarantees the implicit ones will be amortised.

DPDK and Kernel Bypass: Removing the Kernel Entirely

Kernel bypass frameworks such as DPDK take the opposite stance from sendfile. Rather than keeping data inside the kernel, they remove the kernel from the data path entirely. The NIC's receive and transmit queues are mapped into a user-space process via VFIO or UIO, and a poll mode driver reads packet descriptors directly. Packet buffers come from pre-pinned huge page pools, so DMA lands in memory the application already owns.

The performance case rests on per-packet arithmetic. At 100 Gbps, minimum-size 64-byte Ethernet frames arrive at approximately 148.8 million packets per second, or one every 6.7 nanoseconds. The conventional kernel path pays for interrupts and softirq scheduling, sk_buff allocation, protocol layering, socket queueing, and a system call boundary. As a result, a full kernel TCP/IP stack has historically sustained on the order of one to a few million packets per second per core. DPDK applications performing simple forwarding routinely achieve tens of millions per core.

The gains derive from structural choices more than from copy elimination alone. Polling replaces interrupts. Batching amortises descriptor processing and cache misses across bursts of 32 or more packets. Run-to-completion on pinned cores avoids context switches and preserves cache locality. Lockless ring buffers replace contended queues, and huge pages bound TLB pressure. Each technique attacks a fixed per-packet cost, which is why the benefit is greatest where packets are small and numerous.

The costs are equally structural. Polling cores run at full utilisation regardless of load, trading energy and capacity for latency. The kernel's TCP implementation, firewalling, routing, and observability tooling are no longer available. The application must supply its own stack or adopt one, such as F-Stack or Seastar's native stack. The NIC is typically dedicated to a single process, and the operational surface for debugging, isolation, and upgrades expands considerably. Intermediate options exist. XDP and AF_XDP process packets early in the kernel with far less disruption, while io_uring reduces system call overhead without abandoning the kernel stack.

The appropriate domain follows from Amdahl's law. If network stack processing accounts for 10% of per-request CPU time, eliminating it entirely yields at most an 11% speedup. Kernel bypass is justified when the network path is the workload: software routers, load balancers, firewalls, packet capture, network function virtualisation, and latency-critical trading systems. A typical web service is dominated by serialisation, business logic, and database access. For such services, kernel bypass is an expensive answer to a question the profiler is not asking.

Takeaway

Kernel bypass eliminates per-packet costs, so its value is proportional to the share of total work those costs represent. When the network stack is the application, bypass is transformative; when it is incidental overhead, Amdahl's law caps the return.

The three techniques occupy distinct points in a design space. sendfile and splice bypass user space while retaining the kernel's protocol stack and protections. mmap shares memory across the boundary, trading explicit copies for implicit translation and fault costs. DPDK abandons the kernel entirely, trading generality and operational simplicity for per-packet efficiency.

A consistent principle emerges: determine whether costs scale per byte or per operation. Copy elimination addresses per-byte costs and pays off for large payloads where memory bandwidth binds. Kernel bypass addresses per-operation costs and pays off for small, numerous packets where fixed overheads dominate. Applying either technique to the wrong regime yields complexity without throughput.

Data movement is rarely free, but neither is its elimination. Every zero-copy technique relocates cost into page tables, pinned memory, dedicated cores, or lost tooling. The rigorous approach is to quantify the copy, quantify its replacement, and choose the mechanism whose costs fall where the system has slack.