Skip to content

Repository files navigation

SimdLib

SimdLib is a small, header-only library for working with SIMD data and bit-heavy code without scattering compiler intrinsics throughout your project. Its core surface remains C++20; supporting C++23 translation units can additionally use the complete-register value interface.

There is no library binary to build or ship. Add the headers to your project, link the CMake interface target, and use only the pieces you need.

What is included?

  • NativeRegister<T> is the preferred C++23 value interface for operations on one complete target-selected SIMD register.
  • Register<T, Bits> selects an explicit 128-bit or 256-bit representation for stable storage and ABI contracts.
  • RegisterMask<T, Bits> preserves native comparison predicates and provides composition, reduction, observation, and selection operations.
  • NativeApi<T> and Api<Bits, T> remain supported for C++20, compatibility, specialized low-level access, collection helpers, and operations intentionally excluded from Register.
  • SimdVector<T, ElementCount> wraps a register in a fixed-size, value-like container.
  • SimdAlgo applies common operations to arrays and spans.
  • SimdResample packs and expands byte masks, with a scalar fallback when the SIMD path is unavailable.
  • Bmi collects portable and hardware-assisted bit-manipulation helpers.
  • uint128_t provides an unsigned 128-bit value type with formatting support.

SimdLib targets Windows x64 with MSVC or clang-cl and Linux x64 with Clang or GCC. The core requires C++20. GCC 13.2 qualifies the Linux core-only surface; GCC 14 or newer qualifies both the core and Register. Register otherwise requires a supported C++23 compiler with explicit-object member support.

Add it to a project

Place SimdLib in your source tree, for example as a Git submodule, and add it with CMake:

add_subdirectory(external/SimdLib)
target_link_libraries(MyTarget PRIVATE SimdLib::SimdLib)

Link the opt-in target for a C++23 translation unit that uses Register:

target_link_libraries(MyRegisterTarget PRIVATE SimdLib::Register)

Then include the complete public surface. The umbrella exposes Register only when SIMDLIB_REGISTER_INTERFACE_AVAILABLE is nonzero:

#include <SimdLib/SimdLib.h>

Or include a focused header such as <SimdLib/SimdVector.h> or <SimdLib/Bmi.h> to keep dependencies explicit. Formatting support is opt-in through <SimdLib/Format.h>.

A small example

With SSE4.2 enabled, a three-component SimdVector can be used much like a familiar position or math-vector class:

#include <SimdLib/SimdVector.h>

using Vector3 = SimdLib::SimdVector<float, 3>;

Vector3 position{10.0F, 4.0F, 2.0F};
const Vector3 velocity{6.0F, -2.0F, 1.0F};
const float frameTime = 0.5F;

position += velocity * frameTime;
position.z() = 0.0F;

const Vector3 cameraOffset{-3.0F, 2.0F, 1.5F};
const Vector3 cameraPosition = position + cameraOffset;

// cameraPosition is {10.0F, 5.0F, 1.5F}
const float cameraHeight = cameraPosition.z();

Operating on one complete register

Use NativeRegister<T> when the register width may follow the compile target:

#include <SimdLib/SimdLib.h>

using FloatRegister = SimdLib::NativeRegister<float>;

const FloatRegister values = FloatRegister::broadcast(3.0F);
const FloatRegister scale = FloatRegister::broadcast(2.0F);
const FloatRegister offset = FloatRegister::broadcast(1.0F);
const FloatRegister transformed = values * scale + offset;

NativeRegister<T> resolves to 128 bits in an SSE4.2-only translation unit and 256 bits when AVX2 is enabled. Do not store it in an ABI or exchange it across translation units that may use incompatible ISA or SimdLib configuration settings. Use explicit Register<T, Bits> for stable storage, interfaces, and ABI contracts.

On platforms where SimdLib enables a vector calling convention, a non-inlined consumer function must declare the appropriate SIMD_FLAGS(...) boundary mode itself. The annotations on Register members do not propagate to a surrounding function:

using StableFloatRegister = SimdLib::Register<float, 128>;

/**
 * @brief Applies a consumer-defined complete-register transformation.
 * @param value Input register.
 * @return Transformed register.
 */
StableFloatRegister SIMD_FLAGS(InOut) add_one(StableFloatRegister value) noexcept
{
    return value + StableFloatRegister::broadcast(1.0F);
}

Declaring SIMD function contracts

Place SIMD_FLAGS(...) after the return type and immediately before the function name. Every declaration starts with exactly one boundary mode:

Mode Promise Invocation
Neither No native SIMD value, Register, or RegisterMask crosses the boundary by value SIMD_FLAGS(Neither)
In At least one SIMD value enters by value, and no SIMD value is returned by value SIMD_FLAGS(In)
Out A SIMD value is returned by value, and none enters by value SIMD_FLAGS(Out)
InOut SIMD values both enter and leave by value SIMD_FLAGS(InOut)

The optional modifiers follow in the fixed order RegisterOnly, ForceInline, then Flatten:

  • SIMD_FLAGS(InOut, RegisterOnly) promises that every runtime path performs only input reads and register/scalar computation, with no authored write to addressable memory.
  • SIMD_FLAGS(InOut, ForceInline) requests that the annotated function be incorporated into its caller.
  • SIMD_FLAGS(InOut, Flatten) requests recursive inlining of eligible calls made by the annotated function. It does not request that the function itself be inlined into its caller.

For example, a reviewed header-defined register transform may use all three:

/**
 * @brief Adds one to every lane without writing addressable memory.
 * @param value Input register.
 * @return Transformed register.
 */
StableFloatRegister
SIMD_FLAGS(InOut, RegisterOnly, ForceInline, Flatten)
add_one_inline(StableFloatRegister value) noexcept
{
    return value + StableFloatRegister::broadcast(1.0F);
}

The flags are developer promises, not inferred properties. Do not apply RegisterOnly to stores, writable spans, output pointers or references, addressable-buffer algorithms, or functions with unreviewed transitive calls. Declaration and definition flag lists must be ABI-compatible, and translation units that exchange flagged functions must agree on the vectorcall configuration. Out requests the configured calling convention but cannot override a platform ABI that uses hidden return storage.

See the SIMD method-flag contract for declaration forms, compiler mappings, unsupported categories, custom-toolchain adapters, and the qualification required when adding another flag.

Runtime controls for immediate-mode operations

Unsuffixed operations use compile-time controls or genuinely native runtime controls such as selector and mask registers. A method ending in _slow is the explicit runtime-scalar substitute for an immediate-controlled instruction and may require dispatch, branching, or a longer synthesized sequence. See Runtime controls for immediate-mode operations for the complete naming and availability inventory.

Working with RegisterMask

Comparisons create RegisterMask<T, Bits> values. Masks can be combined with &, |, ^, and ~; reduced with any(), all(), or none(); observed as compact lane bits with bits() or as a by-value native predicate through the public native member; and applied with select():

#include <cstdint>

using IntRegister = SimdLib::Register<std::int32_t, 128>;

const IntRegister values = IntRegister::from_lanes(-2, 0, 4, 9);
const auto positive = values.compare_greater(IntRegister::zero());
const auto not_nine = ~values.compare_equal(IntRegister::broadcast(9));
const auto selected_lanes = positive & not_nine;
const auto compact_bits = selected_lanes.bits();
const auto observed_native = selected_lanes.native;
const IntRegister selected =
    selected_lanes.select(values, IntRegister::zero());

Floating comparisons use the selected hardware intrinsic's ordered semantics. A NaN lane is false for the five named comparisons, including compare_equal; positive and negative zero compare equal. Predicate lanes retain their native all-zero or all-one bit patterns.

Transforming a collection

Api::transform applies a register operation across an entire span, including a final partial register. This example converts more than one million local terrain heights into world-space values, then clamps them to the supported vertical range:

#include <SimdLib/Api.h>

#include <numeric>
#include <span>
#include <vector>

using FloatApi = SimdLib::NativeApi<float>;

// Create a large set of example terrain heights.
std::vector<float> localHeights(1'000'003);
std::iota(localHeights.begin(), localHeights.end(), -500'000.0F);

// Prepare values that will be reused for every SIMD batch.
const auto heightScale = FloatApi::set1(0.02F);
const auto seaLevel = FloatApi::set1(64.0F);
const auto minimumHeight = FloatApi::set1(-500.0F);
const auto maximumHeight = FloatApi::set1(8'000.0F);

// Update the entire collection in place, one SIMD batch at a time.
FloatApi::transform(
    std::span<float>{localHeights.data(), localHeights.size()},
    [heightScale, seaLevel, minimumHeight, maximumHeight](
        const FloatApi::vector_t heights) noexcept
    {
        // Convert local heights to world heights.
        const auto scaled = FloatApi::multiply(heights, heightScale);
        const auto worldHeights = FloatApi::add(scaled, seaLevel);

        // Keep every result inside the world's allowed height range.
        const auto aboveMinimum = FloatApi::max(worldHeights, minimumHeight);
        return FloatApi::min(aboveMinimum, maximumHeight);
    });

// Each value is now clamp(localHeight * 0.02F + 64.0F, -500.0F, 8'000.0F).

The executable Register example demonstrates the preferred C++23 complete-register and mask workflows. The separate API example demonstrates the supported C++20 facade, vectors, collection algorithms, bit helpers, uint128_t, resampling, and formatting.

MSVC stack-cookie behavior

Warning

MSVC's default /GS heuristic treats any pointer-free data structure larger than eight bytes as a security-sensitive buffer. Consequently, a non-inlined function that creates or accepts Register<T, bits> by value may receive a security-cookie prologue and epilogue even when __vectorcall transports the value entirely in SIMD registers. This is compiler-generated overhead, not a spill required by the Register representation.

SimdLib marks narrowly audited functions with the RegisterOnly modifier when their runtime path cannot write through pointers, references, spans, arrays, or addressable local buffers. On MSVC, SIMD_FLAGS(..., RegisterOnly, ...) expands to __declspec(safebuffers) and the attribute mapping is empty on other compilers. RegisterOnly remains independent from the In, Out, and InOut boundary modes: stores, transforms, dynamic array-backed fallbacks, and other memory-writing functions retain normal /GS protection. This is a strong developer promise used to justify suppressing /GS for that function, not a compiler-verified guarantee that the function cannot spill or otherwise use the stack.

The operational methods in the Api, Register, RegisterMask, and legacy SimdVector facades use the Flatten modifier to make their transitive-inlining intent explicit. The mapping facades do the same for paths inherited directly by Api. Flattening is an optimization request rather than proof of generated code, so the mandatory codegen gates still compare wrapper and raw-intrinsic objects.

Consumer-defined, non-inlined functions can therefore still encounter this MSVC behavior. Keep /GS enabled globally. Only after reviewing an individual hot function and its generated code should a consumer consider applying __declspec(safebuffers) to that function; the annotation disables /GS protection for the entire annotated function.

The mandatory MSVC generated-code gates compare SSE4.2 and AVX2 wrapper objects with raw-intrinsic mirrors. SSE4.2 is an optimized diagnostic profile; AVX2 is the strict zero-overhead profile. The pure register-only AVX2 subset permits no cookie exception. Its sole optimized exception is the exact 128-bit Register<double>::from_array /GS sequence. The SSE4.2 diagnostic recognizes the corresponding legacy-instruction cookie sequence so the remainder stays comparable. Store, transfer, mutating-reference, opaque-call, and array-return fixtures retain normal /GS protection and paired disassembly for review.

Learn more

  • The wiki contains API documentation for every public method, more examples and usage guidance, supported environments, configuration details, formatting, and development commands.
  • Public namespace and compatibility describes the supported API boundary.
  • Build and validation documents the supported build, test, compiler-matrix, and reporting commands.

License

SimdLib is available under the MIT License.

About

SimdLib is a small, templated C++20 library for working with SIMD data and bit-manipulation. It provides a simple, yet extensive, API wrapper around SIMD intrinsic methods and common algorithms.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages