x64 AVX2 · functional primitives · zero dependencies

Functional programming for C,
at the speed of hand-written assembly.

A small, externally linkable library of reductions, folds, maps, and game math (vec3 / mat4 / quaternion) whose hot paths are hand-tuned x64 AVX2. Pure functions, immutable data, no imperative loops in the inner kernels — wrapped in a thin C API you can drop into a game or graphics engine.

8.6×
faster float reductions than scalar -O3
360
exported functions across 10 numeric types
13
test suites green on Linux & Windows x64
0
external dependencies — links as .a / .so / .dll
Measured, not marketed

Where SIMD actually wins

Every kernel is timed against a scalar C reference compiled at the same -O3 -march=native — so the baseline is autovectorized by the compiler too. The library pulls ahead exactly where the compiler can't: reductions and dot products, where a scalar float accumulator is a serial dependency chain. Memory-bound element-wise ops sit at parity, and the chart says so.

scalar C -O3 (baseline) FP·ASM kernel

method · lower is better · time to process 100,000 f32 elements (µs), best of 200 runs · Intel Core i7-4600M (Haswell, AVX2) · gcc 13 · run it yourself: make bench

Why it's fast

Eight lanes, no dependency chain

A naive for-loop that sums floats can't be reordered by the compiler — floating-point addition isn't associative, so -O3 keeps it strictly serial: each add waits ~4 cycles for the previous one. The assembly kernels keep several independent SIMD accumulators in flight, so the CPU's execution units never stall.

FP·ASM — parallel accumulators

8× f32 added per instruction, across multiple accumulators — the pipeline stays full.

Scalar — serial chain

Each add depends on the last. One lane, latency-bound — the units sit idle waiting.

Built in layers

Assembly at the core, functions on top

Nothing above the kernels re-implements arithmetic — higher layers compose the assembly primitives.

L0asm

AVX2 SIMD kernels

Reductions, fused folds (dot / sum-of-squares / SAD), maps (scale / offset / axpy), prefix scans, and vec3 / mat4 / quaternion math — over 10 numeric types.

L1c

Functional wrappers

Higher-order map / filter / fold, function composition and pipelines, and Maybe / Either monads for total, allocation-free error handling.

L2math

Game & data math

Batched vertex transforms, matrix ops, FFT, radix sort, statistics — composed from the L0 kernels, ready for a renderer or simulation loop.

The whole API is a header away

Drop it into your frame loop

Transform ten thousand vertices and reduce a buffer without writing a single SIMD intrinsic.

// link:  cc game.c -lfpasm      (or add_subdirectory + fpasm::fpasm in CMake)
#include "fp_types.h"
extern void  fp_mat4_mul_vec3_batch(Vec3f* out, const Mat4* m, const Vec3f* in, int n);
extern float fp_fold_dotp_f32(const float* a, const float* b, size_t n);

void update(Vec3f* world, const Mat4* mvp, const Vec3f* local, int n){
    fp_mat4_mul_vec3_batch(world, mvp, local, n);   // AVX2, one pass over n vertices
    float energy = fp_fold_dotp_f32((float*)world, (float*)world, n*3); // 5× a scalar dot
}
Functional, and honest about it

Real closures. A real State monad. Real numbers.

Beyond the fast kernels sits a full functional toolkit — map / filter / foldl / foldr, scanl / scanr / mapAccumL, sortBy / groupBy / traverse / foldMap, Maybe / Either, lazy sequences, transducers — and something most C libraries won't claim: first-class escaping closures and a genuine State monad, in portable C11. No nested functions, no executable stack, AddressSanitizer-clean.

Yes, C can do monads — door #1, not wizardry

A closure is just a code pointer plus a heap-allocated environment plus a destructor — the exact thing GHC, Rust and Swift generate for you. Written by hand, it escapes its scope, composes, and frees cleanly. The State monad (get / put / bind / runState) is built on top of it.

…and here is exactly what it costs — same RNG, three ways

imperative LCG loop1.0×0.36 ms
State monad≈75×27 ms
mapAccumL≈1.6×0.57 ms

All three emit the identical draw sequence. The monad is real but allocates on every bind; mapAccumL is the same stateful computation with zero allocation — its fast path. Run make showcase.

// a pure, reproducible RNG in the State monad — seed IS the state
static ST roll_k(int64_t s){ int64_t s2 = lcg(s);
    return st_then(st_put(s2), st_pure(die_of(s2))); }   // put new seed, yield the die
static ST roll(void){ return st_bind(st_get(), roll_k); }        // do { s <- get; roll_k s }

SR r = st_run(roll(), seed);   // (die, new_seed) — pure, leak-free, gcc & clang
Honest about scope

What it is today — and where it's headed

This is a fast math & FP core, not a game engine. It gives a renderer the pieces that have to be fast; the platform layer is deliberately out of scope — for now.

Shipping now

A tested, cross-platform core

  • Linux + Windows x64, one ABI-abstracted source tree
  • Static & shared libraries via Make and CMake
  • 13 test suites; every kernel checked against a scalar reference
  • Non-executable stack, no runtime allocations in the kernels