Open-source project
aff3ct/MIPP avatar
aff3ct/MIPP

MIPP writes one SIMD call instead of one per instruction set

Portable wrapper for SIMD and vector instructions written in C++11. Compatible with NEON, SSE, AVX, AVX-512 and SVE (length specific).

528 stars99 forksC++MIT

At a glance

What is it?
MIPP is a header-only C++11 wrapper over SSE, AVX, AVX-512, ARM NEON and SVE intrinsics, so the same source compiles to different vector code depending on the target flags. It covers floating point and integer widths from 8 to 64 bits, with a scalar fallback when it cannot match an instruction set, and an optional static library for the compress operation.
Who is it for?
Adopt MIPP when you have a numeric kernel that must run on both x86 and ARM and you would rather write the algorithm once than maintain per-architecture intrinsic code. Do not adopt it if you need the last few percent of hand-tuned SIMD, since an abstraction over intrinsics gives up the control of exactly which instruction is issued, and note that SVE support is still work in progress.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem it removes

MIPP is described as a portable and open-source wrapper, MIT licensed, for vector intrinsic functions, written in C++11, and it works for SSE, AVX, AVX-512, ARM NEON and SVE instructions, with SVE marked as work in progress.

The problem it removes is stated in one sentence in the README: with the MIPP wrapper you do not need to write a specific intrinsic code anymore, you just use the provided functions and the wrapper automatically generates the right intrinsic calls for your specific architecture.

That is the whole value proposition, and it is worth being concrete about what it saves. A kernel written against raw intrinsics needs one implementation per instruction set, often more than one per instruction set because AVX and AVX-512 have different register widths, and the code has to be guarded by preprocessor conditionals. MIPP replaces that with one implementation and a set of compiler flags that select the backend.

The type coverage is stated too: simple and double precision floating point numbers, and signed and unsigned integer arithmetic at 64-bit, 32-bit, 16-bit and 8-bit. So it covers the element widths that general numeric code actually uses, not just the floating point case.

There is also an entry point for the library from the AFF3CT project, which is the context: this is a communications and signal processing toolkit, and the wrapper was extracted for general use.

The releases are recent and descriptive, which tells you about the direction of travel. v2.2.0 from 2026-10-18 is titled Half-Vectors, LMUL-Changing Functions and API Cleanup, v2.1.0 from 2026-10-08 is Fine-Grained Modular Headers, Object API Polish and API Explorer Upgrade, and v2.0.3 from 2026-09-21 came before those. The LMUL work is SVE-specific, since LMUL is the vector length multiplier in the SVE model.

The project is C++, MIT licensed, with the last push on 2026-10-18, and the repository is on GitLab with the coverage report published at aff3ct.gitlab.io.

Installing it means a header and a flag

There is nothing to install, because MIPP is a single header file, and the headers are located in the `include` folder. The location changed at one point, and the README notes that before commit `6795891` they were in the `src` folder.

You include it in the sources that need it:

cpp
#include "mipp.h"

The header uses a C++ namespace, `mipp`, and if you do not want to prefix every call with `mipp::` you can bring it into scope:

cpp
#include "mipp.h"
using namespace mipp;

Then the compiler has to be told which vector instructions you want. For GNU with an SSE or AVX capable CPU you add `-march=native`. For ARMv7 CPUs with NEON instructions you add `-mfpu=neon`, and the README gives the reason, that most current NEONv1 instructions are not IEEE-754 compliant. That is no longer the case on ARMv8 processors, so `-march=native` works there too. And because MIPP uses features provided by C++11, the `-std=c++11` flag has to be added.

The supported compiler list is worth reading, because it defines the floor: Intel `icpc` 16 or newer, GNU `g++` 4.8 or newer, Clang `clang++` 3.6 or newer and Microsoft `msvc` 14 or newer.

And there is a performance note that is unusually specific. On `msvc` 14.10, Microsoft Visual Studio 2017, the performances are reduced compared to the other compilers because the compiler is not able to fully inline all the MIPP methods. That was fixed on `msvc` 14.21, Visual Studio 2019, and the README says you can now expect high performances.

So the practical requirement is a compiler that inlines aggressively, and on MSVC that means 2019 or newer rather than 2017.

CMake integration

If MIPP is installed on the system it can be integrated into a CMake project in the standard way, and the README gives both halves.

Installing it:

sh
# install MIPP
cd MIPP/
export MIPP_ROOT=$PWD/build/install
cmake -B build -DCMAKE_INSTALL_PREFIX=$MIPP_ROOT
cmake --build build -j5
cmake --install build

And consuming it:

cmake
# find the installation of MIPP on the system
find_package(MIPP REQUIRED)

# define your executable
add_executable(gemm gemm.cpp)

# link your executable to MIPP
target_link_libraries(gemm PRIVATE MIPP::mipp)

The `FindMIPP.cmake` module at the repository root is what makes `find_package(MIPP REQUIRED)` work, and the imported target is `MIPP::mipp`. Using a namespaced target rather than a bare library name is the modern CMake convention and means the include directory and the compile definitions travel with the target rather than being set globally.

The consuming side has a detail that decides whether the build finds the library at all:

sh
cd your_project/
# if MIPP is installed in a system standard path: MIPP will be found automatically with cmake
cmake -B build
# if MIPP is installed in a non-standard path: use CMAKE_PREFIX_PATH
cmake -B build -DCMAKE_PREFIX_PATH=$MIPP_ROOT

That is the standard CMake search behaviour, but it is worth noting because MIPP is the sort of dependency you often install into a prefix of your own rather than into the system, and the failure when the prefix is non-standard is a configure error rather than a compile error.

The static library is optional, and one operation needs it

MIPP is mainly a header only library, but some macro operations require compiling a small library. The README names the operation: `compress`, which relies on generated lookup tables stored in the static library.

Generating those tables needs Python 3 with the Jinja2 package:

bash
sudo apt install python3 python3-pip
pip3 install --user -r codegen/requirements.txt

Then you run the generator:

bash
python3 codegen/gen_compress.py

And compile the static library:

bash
cmake -B build -DMIPP_STATIC_LIB=ON
cmake --build build -j4

The README is explicit that the compilation of the static library is optional, and that if you choose not to compile it then only some macro operations will be missing.

That is a sensible design for a header-only library, and it is worth being clear about the trade. A pure header dependency means no build step, no link step and no ABI to worry about, which is what makes it easy to drop into an existing project. The cost is that the operations that genuinely need precomputed tables cannot be pure, and those are exactly the ones with the largest code size, such as a vectorised sort of a small fixed-width key.

So the question to ask is whether your code uses the compress operation or the sort. The repository has a `examples/sort/` directory alongside the other examples, so the sort path is worth a look before you decide the static library is unnecessary.

The scalar fallback and how to check it

MIPP does not fail when it meets an instruction set it does not know, which is the behaviour that makes it usable in a portable codebase.

By default, MIPP tries to recognize the instruction set from the preprocessor definitions. If it cannot match the instruction set, for instance when MIPP does not support the targeted instruction set, it falls back on standard sequential instructions. In that mode the vectorization is not guaranteed anymore, but the compiler can still perform auto-vectorization.

That last sentence is the important part of the fallback. Falling back to sequential code does not mean falling back to slow code, because the compiler is still free to vectorize the loop it sees. What you lose is the explicit register control, so the compiler is choosing the vectorization strategy rather than the wrapper.

You can force the sequential mode with the compiler definition `-DMIPP_NO_INTRINSICS`, and the README says it can sometimes be useful for debugging or to benchmark a code.

There is also a way to check what the configuration actually resolved to. You can print the global variable `mipp::InstructionFullType`, which is a `std::string`.

So a sensible workflow when you hit a performance problem is to print that variable, confirm which backend was selected, and if it fell back to scalar when you expected vector code, look at the compiler flags rather than at the source. A `-march` flag that does not match the CPU, or a toolchain that does not define the expected preprocessor macros, produces a silent scalar build rather than an error.

Registers, lanes and memory access

The programming model is small, and it is the part you will use every day.

You declare registers with the `mipp::Reg<T>` type:

cpp
mipp::Reg<T> r1, r2, r3; // we have declared 3 vector registers

The number of elements per register depends on the data type, and you get it from the `mipp::N<T>()` function, where `T` is a template parameter and can be `double`, `float`, `int64_t`, `uint64_t`, `int32_t`, `uint32_t`, `int16_t`, `uint16_t`, `int8_t` or `uint8_t`. The idiomatic loop steps by that count:

cpp
for (int i = 0; i < n; i += mipp::N<float>()) {
	// ...
}

That is a nice property of the design. The loop bound and the step are expressed in terms of the same quantity, so the code is correct for any of the ten element types and for any instruction set, without a macro for the vector length.

Memory access works through `load` and `store`, and the `load` call can be implicit because the register has a constructor taking a pointer:

cpp
mipp::Reg<float> r1 = &myVector[i*mipp::N<float>()];

with `store` as a method on the register. The alignment behaviour is configurable, and the default is the one worth knowing. By default the loads and stores work on unaligned memory. With the `-DMIPP_ALIGNED_LOADS` definition they work on aligned memory by default, and in that aligned mode you can still perform unaligned operations with the `mipp::loadu` and `mipp::storeu` functions. The reverse is not true: it is not possible to perform aligned loads and stores in the unaligned memory mode.

For allocating aligned data there is an aligned memory allocator wrapped into the `mipp::vector` class, which the README says is fully retro-compatible with the standard `std::vector` class and can be used everywhere you can use `std::vector`.

So the practical pattern is a `mipp::vector` for the buffer, a `mipp::Reg<T>` for the working set, and a loop that steps by `mipp::N<T>()`.

Where the abstraction costs you

The honest limit of MIPP is that a portable wrapper cannot express everything the hardware can do, and you give up control of the instruction stream in exchange.

The most visible cost is on the newest instruction sets. SVE is described as work in progress, and the repository description notes that SVE support is length specific, which is the fundamental property of that architecture: the vector length is not known at compile time, so any wrapper has to carry the length around at run time. The v2.2.0 release title, with LMUL-changing functions, is the work in progress showing through.

The second cost is that you write against the intersection of the instruction sets unless you add architecture-specific code behind the same API yourself. An AVX-512 masked operation or an SVE predicate has no equivalent in SSE, so either the wrapper omits it or the wrapper grows conditionals.

The third is the compiler. MIPP's performance depends on the compiler inlining its methods, and the MSVC 2017 case is the documented example of it not happening. That is a general property of header-heavy C++ abstractions, and it is why the README bothers with a compiler support matrix.

The alternative approach is to write the intrinsics yourself, once per target, behind your own thin interface. That gives you the full instruction set on each architecture and the performance of a hand-written kernel, at the cost of the maintenance MIPP exists to remove. The middle option is to let the compiler auto-vectorize ordinary loops and write no intrinsics at all, which is the right answer for a lot of code and which MIPP itself falls back on when it cannot match an instruction set.

Where MIPP earns its place is the middle ground: numeric kernels where you want one implementation, several architectures in the field, and a need to write the vector code by hand rather than hope the compiler finds it.

Editorial conclusion

Adopt MIPP when you have a numeric kernel that must run on both x86 and ARM and you would rather write the algorithm once than maintain per-architecture intrinsic code. Do not adopt it if you need the last few percent of hand-tuned SIMD, since an abstraction over intrinsics gives up the control of exactly which instruction is issued, and note that SVE support is still work in progress. Verify first that your compiler is one of the tested ones, Intel `icpc` 16 or newer, GNU `g++` 4.8 or newer, Clang `clang++` 3.6 or newer or MSVC 14 or newer, that your build passes the right `-march` flag, and that you do not need the `compress` operation unless you build the optional static library.

Frequently asked questions

What is MIPP?

MIPP is a portable, MIT licensed wrapper for vector intrinsic functions written in C++11. It works for SSE, AVX, AVX-512, ARM NEON and SVE, with SVE in work in progress, and covers simple and double precision floating point plus signed and unsigned integer arithmetic at 64, 32, 16 and 8 bits.

How do I use MIPP in my code?

It is a header-only library, so you include `mipp.h` and tell the compiler which instruction set you want with `-march=native`, or `-mfpu=neon` on ARMv7, plus `-std=c++11`. The calls live in the `mipp` namespace, and you can bring it into scope with `using namespace mipp;`.

Which compilers does MIPP support?

The README lists Intel `icpc` 16 or newer, GNU `g++` 4.8 or newer, Clang `clang++` 3.6 or newer and Microsoft `msvc` 14 or newer, with a note that performance on `msvc` 14.10 was reduced because the compiler could not fully inline the MIPP methods, fixed in `msvc` 14.21.

Do I need to build the MIPP static library?

No, the compilation of the static library is optional. It is only needed for some macro operations, in particular `compress`, which relies on generated lookup tables. To build it you install Python 3 with Jinja2, run `python3 codegen/gen_compress.py` and then configure with `-DMIPP_STATIC_LIB=ON`.

What happens when MIPP meets an unsupported instruction set?

By default MIPP tries to recognize the instruction set from the preprocessor definitions and, if it cannot match, falls back on standard sequential instructions. In that mode vectorization is not guaranteed but the compiler can still auto-vectorize, and you can force the mode with `-DMIPP_NO_INTRINSICS`.

Official sources

  1. aff3ct/MIPP on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/aff3ct-mipp.svg)](https://hysenlabs.com/projects/aff3ct-mipp)

Was this useful?