# aklomp/base64: a C99 Base64 codec that picks its SIMD path at runtime

> A single-file, allocation-free Base64 library in C99 that detects AVX2, AVX512, SSSE3, SSE4.1, SSE4.2, AVX and NEON at runtime on x86 and at compile time on ARM. The catch is that the fastest x86 codecs are opt-in at build time through environment variables, and AVX512 is encode-only.

**aklomp/base64** — Fast Base64 stream encoder/decoder in C99, with SIMD acceleration

- Repository: https://github.com/aklomp/base64
- Stars: 1,023 · Forks: 179
- Language: C
- License: BSD-2-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/aklomp-base64

## What aklomp/base64 replaces in a C program

Base64 is one of those pieces of plumbing that nobody wants to own. Most C codebases end up with a small ad-hoc encoder, often a bytewise loop with a lookup table, and inherit whatever bugs that loop has. aklomp/base64 is a complete stream codec for that job, written in C99, plus wrapper functions for encoding and decoding simple length-delimited strings.

The scope is deliberately narrow and that is the point. The library reads and writes blocks of streaming data, which means you can feed it a socket or a file without materializing the whole payload. It does not dynamically allocate memory. It is re-entrant and threadsafe. The project describes itself as unit tested, and the repository carries a `test/` directory along with a continuous integration badge in the header.

The README also states that the code is valid C99 that compiles with pedantic options on, and the Makefile confirms it: the base `CFLAGS` line carries `-std=c99 -O3 -Wall -Wextra -pedantic`. That is a good signal for a library you intend to vendor into a project with a strict warning policy.

One implementation detail is unusual enough to mention: the project uses Duff's Device. It is a real technique for unrolling loops with a computed trip count, and seeing it in a production codec is a reminder that this is a performance-minded library written by someone who cares about instruction-level results.

## Runtime feature detection on x86, compile-time NEON on ARM

The dispatch mechanism differs by architecture, and the difference matters when you decide how to package your binary.

On x86 the library does runtime feature detection. The first time a codec is called, the library determines the appropriate encoding and decoding routines for the machine and remembers them for the lifetime of the program. If the processor supports AVX2, SSSE3, SSE4.1, SSE4.2 or AVX, the library picks an optimized codec that encodes or decodes 12 or 24 bytes at a time, which the README says gives a speedup of four or more times compared with the plain bytewise codec. The `lib/codec_choose.c` file in the source tree is where that choice is made, and the `lib/arch/` tree holds one directory per codec family: `avx512`, `avx2`, `generic`, `neon32`, `neon64`, `ssse3`, `sse41`, `sse42` and `avx`.

On ARM the story is different. NEON support is hardcoded to on or off at compile time because portable runtime feature detection is unavailable on ARM from userland: the `mrc` instruction that would read the CPU feature register is privileged. The README explains the workaround, which is that you can include the NEON codec, make it the default, and then force a downgrade at runtime with `BASE64_FORCE_PLAIN` if your own detection says the CPU lacks NEON.

The two NEON codecs are separate, one for 32-bit ARM and one for 64-bit AArch64, the latter having double the SIMD register count and being able to do full 64-byte table lookups. They encode in 48-byte chunks and decode in 64-byte chunks, so an extra uint32 and uint64 codec was added to keep small inputs fast.

## AVX512 is encode-only and the decoder reuses AVX2

This is the most important limitation for anyone planning around the widest x86 support, and it is stated plainly in the README. AVX512 support is only for encoding at present, using the AVX512 VL and VBMI instructions, and the decoding side reuses the AVX2 implementations. The README also notes that CPUs later than Cannonlake, manufactured in 2018, support these instructions.

So on a modern server CPU with AVX512, base64 encoding gets the wide path and base64 decoding does not. Any workload that is decode-heavy, which is the common shape of a service that accepts Base64 payloads, gets no benefit from the newest instruction set even though the code is compiled in.

A second wrinkle sits on top of this. The README says the codec will only be used if runtime feature detection shows that the target machine supports it, but the AVX512 section repeats the AVX2 wording about AVX2 specifically. In other words, the inclusion of a codec in the binary and its actual use at runtime are two separate questions, and the build flags answer only the first.

The AVX512 encoder itself is not original work. The README credits it as based on code from Wojciech Muła's `base64simd` library, and the SSSE3 and AVX2 codecs were substantially improved using optimizations described in Muła's articles on SSE Base64 encoding and decoding.

## The build is driven by environment variables, not by autodetection

The build system is plain Make. The `lib` directory holds the library code, `include/libbase64.h` is the header you need, and typing `make` in the top level directory builds two things: `lib/libbase64.o`, a single self-contained object file you can link into your own project, and `bin/base64`, a standalone test binary that works similarly to the `base64` system utility.

To compile only the plain library, without any SIMD codecs, the README gives:

```sh
make lib/libbase64.o
```

Optional codecs are pulled in by setting a matching environment variable, and the Makefile shows exactly how: it tests each of `AVX512_CFLAGS`, `AVX2_CFLAGS`, `NEON32_CFLAGS`, `NEON64_CFLAGS`, `SSSE3_CFLAGS`, `SSE41_CFLAGS`, `SSE42_CFLAGS` and `AVX_CFLAGS` for presence, and when one is set it writes a matching `HAVE_` define into the generated `lib/config.h`. A typical x86 invocation looks like this:

```sh
AVX2_CFLAGS=-mavx2 SSSE3_CFLAGS=-mssse3 SSE41_CFLAGS=-msse4.1 SSE42_CFLAGS=-msse4.2 AVX_CFLAGS=-mavx make lib/libbase64.o
```

The single-codec form is the usual one. For AVX2:

```sh
AVX2_CFLAGS=-mavx2 make
```

And for the AVX512 encoder:

```sh
AVX512_CFLAGS="-mavx512vl -mavx512vbmi" make
```

The `OPENMP` variable is a separate switch. When it is set, the Makefile appends `-fopenmp` to `CFLAGS`, which enables the parallel path contributed by Ferry Toth from Exalon Delft.

## Choosing between the four NEON build strategies

The README lays out the NEON options as an explicit list of four, and picking the wrong one is easy because option 4 is described as making no sense.

Option 1 is to not include NEON support at all, which means specifying no NEON-specific compiler flags:

```sh
CC=clang CFLAGS="-march=armv6" make
```

Option 2 is to build NEON support and make it the default, while building all other code without NEON flags, so that you can override the default at runtime with `BASE64_FORCE_PLAIN`. The README's own example for this builds the whole tree at ARMv6 level except the NEON codec. Option 3 is to build everything with NEON support and make it the default. Option 4 is to build everything with NEON but not make it the default.

Option 2 is the interesting one for a fleet of devices, because it is the only strategy that gives you a single binary with a runtime escape hatch. The cost is that you must write the detection yourself.

The compiler recommendation is equally specific: use LLVM/Clang for the NEON codecs. The README says the code generation of at least GCC 4.6, the version shipped with Raspbian and used for testing, contains a bug when compiling `vstq4_u8()`, and that the generated assembly is of low quality, calling NEON intrinsics a known weak area of GCC. Clang does a better job.

## Where a stock base64 utility is the better answer

There is a real alternative for most Base64 workloads, and it ships with your operating system. The `base64` system utility is the reference implementation that the project's own `bin/base64` test binary is described against. It is already on the machine, it handles the same alphabet, and it costs nothing to add.

The difference in approach is that the system utility is a process, and aklomp/base64 is a linkable object. If you are base64-encoding a config value once at start-up, forking a process is wasteful and linking the library adds a build dependency for no gain. The library earns its place when Base64 sits inside a hot loop, when you want to stream data in blocks without buffering the whole payload, or when you need deterministic control over memory usage in an embedded or latency-sensitive context.

The allocation behaviour is the clearest dividing line. The library does not dynamically allocate memory, which the README lists as a notable feature, and the Makefile compiles the code with `-DBASE64_STATIC_DEFINE` and links the object files into a single archive with `ld -r` plus an `objcopy` step that keeps only the symbols listed in `lib/exports.txt`. On MinGW32 the export names are mangled with a leading underscore, which the Makefile handles with a `sed` rule.

For a simple script or a build-time step, the standard library function or the command line tool remains the correct choice. The library's own README does not claim to be a general purpose base64 replacement for that case.

## OpenMP parallelism, licensing and release cadence

Parallelism is opt-in through the `OPENMP` environment variable, which adds `-fopenmp` to the compiler flags. The README lists OpenMP among the notable features, phrased as even more parallel speedups, but it does not give a threshold, a chunk size or any guidance about when parallelizing beats the SIMD path. The interaction between an OpenMP loop and a SIMD codec is exactly the kind of thing that needs measurement on your own payloads and core counts, and the README does not attempt it. The repository does include a `base64-benchmarks.png` image at the top level, which is the only benchmark artefact visible in the tree, though the numbers behind it are not transcribed.

The licence is BSD-2-Clause, a permissive licence with a short grant, and the LICENSE file sits at the repository root. The `LICENSE` directory listing in the tree confirms it. Anyone redistributing the library inside a larger product should read that file rather than a summary of it.

As for upstream maintenance, the repository is not archived and the last push was on 2026-10-06, so work is landing. The release tags tell a different story: the recent releases are v0.5.2 from 2024-01-10, v0.5.1 from 2023-11-08 and v0.5.0 from 2022-08-19. A project with a `CMakeLists.txt` and a `cmake/` directory in the tree alongside the Makefile, but with the newest tag two and a half years behind the last push, means vendoring from a tag and vendoring from master give you different code. The README itself names an author article on SSE Base64 as the place to understand the approach, and credits the original AVX2, NEON and Aarch64/NEON codecs to the Inkymail fork, with that work slowly being backported.

## Conclusion

aklomp/base64 is the right pick for C and C++ programs that encode or decode large Base64 payloads on the hot path, especially when you control the build machine and can opt into AVX2 or SSSE3 with a single environment variable. It is the wrong pick for a script that needs one-shot encoding, for a build that must run identically on unknown x86 hardware, and for ARM fleets where NEON detection has to be done by hand, since the `mrc` instruction is privileged and the default is fixed at compile time. Before adopting it, verify that your compiler accepts the codec flags you intend to set, confirm that the `OPENMP` variable is worth the parallel path for your payload size, and check whether the v0.5.2 tag from 2024-01-10 still matches the source tree you are vendoring, since the last push was on 2026-10-06.

## FAQ

### How do I enable the AVX2 codec when building aklomp/base64?

Set the `AVX2_CFLAGS` environment variable to the compiler flag that turns on AVX2, typically `-mavx2`, and run make. The Makefile detects the variable and writes a matching `HAVE_AVX2` define into the generated `lib/config.h`.

### Does aklomp/base64 support AVX512 decoding?

No. AVX512 is used for encoding only, through the AVX512 VL and VBMI instructions, and the decoding side reuses the AVX2 implementations.

### How is the NEON codec selected on ARM devices?

NEON support is decided at compile time because the `mrc` instruction needed for runtime detection is privileged. You can build the NEON codec as the default and then force a downgrade at runtime with `BASE64_FORCE_PLAIN` after doing your own detection.

### What files do I need to link aklomp/base64 into my project?

Run `make` in the top level directory, which builds `lib/libbase64.o`, a single self-contained object file, and include `include/libbase64.h`. The `bin/base64` binary is only a test binary that behaves similarly to the `base64` system utility.

### Can I use OpenMP with aklomp/base64?

Yes. Set the `OPENMP` environment variable, which makes the Makefile add `-fopenmp` to `CFLAGS`. The OpenMP implementation was contributed by Ferry Toth from Exalon Delft.

### Does aklomp/base64 allocate memory while encoding?

No. The README lists as a notable feature that the library does not dynamically allocate memory, and it reads and writes blocks of streaming data so you do not need to buffer the whole payload.

## Sources

- [aklomp/base64 on GitHub](https://github.com/aklomp/base64)
- [Issues](https://github.com/aklomp/base64/issues)
- [License: BSD-2-Clause](https://github.com/aklomp/base64/blob/master/LICENSE)
- [README](https://github.com/aklomp/base64/blob/master/README.md)
- [Releases](https://github.com/aklomp/base64/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aklomp-base64
