How memcpy Achieves High Speed with Vectorization
The standard C library function memcpy maximizes data
transfer throughput by moving away from simple byte-by-byte copy loops
and leveraging architecture-specific SIMD (Single Instruction, Multiple
Data) vectorization. Rather than transferring one to eight bytes at a
time, modern implementations dynamically detect CPU capabilities to
execute wide register transfers, enforce memory alignment, and bypass
cache hierarchies when appropriate. This article breaks down the
internal mechanics of vectorized memcpy routines, examining
how standard C libraries harness hardware registers, handle alignment,
and optimize data throughput across diverse CPU architectures.
The Limitation of Scalar Copying
A naive implementation of memcpy increments pointers and
transfers one byte, or at best a machine word (32 or 64 bits), per loop
iteration. This approach severely underutilizes the memory bus and CPU
execution pipelines. Modern processors feature execution units capable
of handling 128-bit, 256-bit, or 512-bit operations in a single cycle.
Relying solely on general-purpose registers (such as RAX on
x86-64 or X0 on ARM64) introduces significant instruction
overhead, saturation of execution ports, and inefficient memory bus
saturation for large payloads.
SIMD Registers and Parallel Transfer
High-performance memcpy implementations in libraries
like GNU C Library (glibc) and musl replace scalar loops with vector
instructions:
- x86/x86-64 Architectures: Implementations utilize
SSE (128-bit
XMMregisters), AVX/AVX2 (256-bitYMMregisters), and AVX-512 (512-bitZMMregisters). For example, an AVX2-optimized copy routine utilizes instructions such asvmovdquto load and store 32 bytes in a single instruction. - ARM Architectures: Implementations leverage ARM
NEON (128-bit
Vregisters) and SVE (Scalable Vector Extension), using load/store pair instructions (LDP/STP) or vector loads (VLD1/VST1) to move chunks of 16 to 64 bytes concurrently.
By utilizing wide registers, a vectorized loop can transfer hundreds of bytes per unrolled iteration, dramatically reducing total branch evaluations and instruction decoding overhead.
Handling Alignment: Head, Body, and Tail Strategy
Vector instructions perform most efficiently when memory addresses
match hardware alignment boundaries (typically 16, 32, or 64 bytes).
Unaligned vector access can span across cache lines, triggering
performance penalties. To mitigate this, vectorized memcpy
employs a three-phase copy strategy:
- The Head (Prolog): The routine inspects the source and destination pointers. It copies small, scalar amounts of data (or uses unaligned smaller vector loads) until the destination pointer is aligned to the target vector boundary (e.g., a 32-byte boundary for AVX2).
- The Body (Main Loop): Once aligned, the function executes an unrolled loop that processes large blocks using aligned vector stores. Aligning the destination is prioritized over the source because unaligned writes typically penalize hardware pipelines more severely than unaligned reads.
- The Tail (Epilog): Any remaining bytes that do not fit into a full vector register are handled using overlapping vector registers, smaller vector subsets (e.g., falling back from 256-bit to 128-bit or 64-bit loads), or small scalar jumps. This eliminates the need for slow single-byte residual loops.
Dynamic Architecture Dispatching (IFUNC)
Because a single compiled binary may run on various generations of
CPUs, memcpy does not rely on a static instruction set. On
modern Linux systems using glibc, dynamic dispatch is achieved through
Indirect Functions (GNU IFUNC).
During program startup and dynamic linking, the runtime queries the
processor via the CPUID instruction on x86 or auxiliary
vectors (getauxval) on ARM. The resolver identifies CPU
flags (such as AVX2, AVX512F,
ERMS, or NEON) and binds the
memcpy symbol directly to the fastest routine supported by
the host machine. Once resolved, subsequent calls invoke the optimal
vector implementation directly without branch-check overhead.
Cache Bypassing with Non-Temporal Stores
When copying exceptionally large memory buffers—often those exceeding the size of the CPU's Last Level Cache (L3)—standard vector stores can cause cache thrashing by evicting useful program data.
To prevent this, vectorized memcpy implementations
switch to non-temporal store instructions (such as
vmovntdq on x86). Non-temporal stores write directly to
system RAM through write-combining buffers, bypassing the CPU cache
hierarchy entirely. This preserves cache lines for other application
operations and maximizes raw memory bus throughput for multi-megabyte
transfers.
Enhanced REP MOVSB (ERMS)
On recent x86 processors, microcode enhancements have made string-manipulation instructions competitive with vector registers. Intel and AMD introduced Enhanced REP MOVSB/STOSB (ERMS) and Fast Short REP MOVSB.
For certain buffer sizes and modern microarchitectures, glibc
dynamically dispatches memcpy to use
rep movsb. The processor's internal hardware handles the
wide register vectorization within the microcode engine itself, freeing
general-purpose vector execution units for concurrent application
workloads.