fastmem API reference Changelog View on GitHub

Zig programs get memcpy, memmove, and memset from compiler-rt. compiler-rt's memset is a byte loop, and its memcpy and memmove are slower than glibc on most CPUs. fastmem replaces them with kernels tuned per CPU model, measured against glibc and compiler-rt on seven EC2 targets.

You can call the kernels directly, or export them as the memory symbols for a link unit. The library imports only std and builds with Zig 0.16.0. The API reference documents every public symbol.

Getting started

Add the package to build.zig.zon:

zig fetch --save git+https://github.com/baskingspot/fastmem

Then add the module:

const fastmem = b.dependency("fastmem", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("fastmem", fastmem.module("fastmem"));

Explicit calls

Call fastmem where you want it. Nothing else in the program changes.

const fastmem = @import("fastmem");

fastmem.copy(u8, dest, source); // non-overlapping; dest.len >= source.len
fastmem.move(u8, dest, source); // overlap-safe
fastmem.set(u8, dest, 0);

copy, move, and set are generic over the element type, and the slice lengths are in elements. copy has the memcpy contract: the ranges must not overlap, and dest must hold at least source.len elements. move allows overlap. set writes value to every element. For T other than u8, the vector kernel applies only when T has a unique in-memory representation and the value's bytes are all equal; the second check runs at runtime. Every other value takes an element-wise loop. The functions are inline. Small sizes compile to straight-line code at the call site. Larger sizes call the kernel directly, with no PLT and no ifunc.

Debug builds assert the length and overlap contracts for copy and move.

Replacing the symbols

To make every @memcpy, @memmove, and @memset in a link unit use fastmem, add one line to the root source file:

comptime { @import("fastmem").exportSymbols(); }

This gives the link strong, hidden memcpy, memmove, and memset definitions. They win over compiler-rt and glibc. The export applies to aarch64 and x86_64 ELF targets, and it needs the LLVM backend. In Debug builds, set exe.use_llvm = true in the build, or pass -fllvm to zig build-exe.

Call exportSymbols() from the program that owns the final link, not from a library that another program imports. A library that exports the symbols adds strong definitions to every consumer's link, and a consumer with its own definitions gets a link error. A shared library that you build and ship can opt in for its own link unit.

Remove your own definitions of the three symbols before you enable the export. A second definition is a compile or link error. The export also covers C objects in the same link unit; a C compiler can still inline an operation. It does not replace fortified aliases such as __memcpy_chk, and it does not reach into an already-built shared library.

Small or constant-size builtins can stay inline, so the export replaces the operations that become symbol calls, not every builtin.

Kernels

TargetKernel
x86_64 with AVX-512Zig vector kernels with per-model tuning: rep movsb and non-temporal thresholds, size-class dispatch, zmm registers without vzeroupper
x86_64 with AVX2 (x86_64_v3)the same kernels with ymm
x86_64 Linux without AVX2 (baseline)the kernels above, selected at run time from CPUID (see Runtime dispatch); the generic fallback on hosts without AVX2
aarch64 with SVEArm Optimized Routines memcpy-sve / memset-sve, plus per-model small-size paths
aarch64 without SVEArm Optimized Routines AdvSIMD
other targetsa generic Zig vector fallback

The aarch64 kernels are ports of Arm Optimized Routines (MIT), and src/x86_64/compact.zig adapts Zig compiler-rt. THIRD_PARTY.md lists every port, its pinned revision, and its license.

fastmem.impl names the kernel that a build selected:

std.debug.print("{s} {s} {s}\n", .{ fastmem.impl.copy, fastmem.impl.move, fastmem.impl.set });

In a runtime-dispatch build, fastmem.impl names the stub family and fastmem.dispatch.kernelName() names the kernel that the host selected.

Runtime dispatch

An x86_64 Linux build whose target CPU lacks AVX2 selects its kernels at run time. One -Dcpu=baseline binary runs the AVX-512 kernels on an AVX-512 host, the AVX2 kernels on an AVX2 host, and the generic kernels on anything older.

Dispatch is active when all of these hold:

Every other build selects its kernels at compile time and contains no dispatch code.

LevelRequired featuresSelected on
genericnonehosts without the full x86-64-v3 feature set
x86_64_v3x86-64-v3AVX2 hosts, including Skylake-SP, Cascade Lake, Cooper Lake, and Ice Lake
x86_64_v4x86-64-v4AVX-512 hosts that are not one of the model rows
sapphirerapidsx86-64-v4Intel family 6, model 0x8f
graniterapidsx86-64-v4Intel family 6, model 0xad
znver4x86-64-v4AMD family 0x19, Zen 4 model ranges
znver5x86-64-v4AMD family 0x1a

The model rows carry the REP and non-temporal thresholds measured on that model. An unknown AVX-512 model gets x86_64_v4, which cannot select REP on AMD and cannot start non-temporal stores below the cache size.

The API reports what the host selected:

if (fastmem.dispatch.enabled) {
    const level = fastmem.dispatch.level().?;
    const name = fastmem.dispatch.kernelName().?;
    const facts = fastmem.dispatch.detect().?;
}

level returns ?Level, kernelName returns ?[:0]const u8, and detect returns the CPUID and XCR0 facts that the selection read. All three return null in a build without dispatch. force is a test hook; the public module does not provide it.

To turn dispatch off, pass the dependency option:

const fastmem = b.dependency("fastmem", .{
    .target = target,
    .@"x86-dispatch" = false,
});

A dispatch build inlines sizes up to 128 bytes. Larger sizes call the selected kernel through a pointer, with no per-call CPUID check. The dispatcher reads CPUID and XCR0 once, on the first call.

Correctness

The guard-page suite runs about 28 million cases per build. Each case places the source or destination against a guard page, so an over-read, an over-write, or a wrong direction faults. The suite runs in ReleaseFast, ReleaseSafe, and Debug, on the target hardware.

The codegen gates check the generated code, not only its behavior. The x86 gate checks vector widths, the ABI entry shape, and that no kernel calls a memory symbol. It runs for every fleet CPU model. The aarch64 gate pins each kernel's instruction bytes to the measured GOLDEN set, so a kernel change needs a deliberate update of those bytes.

The export layer has its own binary checks: strong and hidden definitions, no memcpy/memmove/memset references inside the kernels, no entries in .dynsym, and no recursion through Zig helpers. The checks link with and without libc, with and without compiler-rt, and in Debug, ReleaseSafe, and ReleaseFast.

Run the checks from the repository:

just test          # unit tests, export checks, shipped-binary builds
just test-guard    # the full guard-page matrix on this host
just codegen-x86   # the x86 codegen gates for every fleet CPU model

Results

docs/results/scorecard.md is the current state against glibc 2.40 and compiler-rt on seven EC2 targets: c7i, c8i, c7a, c8a, c7g, c8g, and c9g. Each cell is a time ratio, a geometric mean over a size tier, and a value below 1 means fastmem is faster. The tables separate the ABI calls from the inline calls, and glibc from compiler-rt.

The scorecard records its run directories, the main revision, the six rounds, the A/A floor per target, and the correctness run behind the numbers. The largest win is memset against compiler-rt: the above-16 KiB tier is 0.04 to 0.12 across the seven targets. Copy and move have smaller margins; the scorecard has the per-tier numbers.

docs/results/baseline-0.16.md measures the problem the library started from: compiler-rt against glibc. docs/results/ holds one file per fleet measurement. A performance claim in this repository names a run directory and a results file.

Limits