Backend architecture

The root and Wide packages define scalar results. Their native packages implement or compose matching operations. Runtime selection occurs only for whole-buffer operations.

Portability has three separate meanings

  1. The public operation has the same semantics on each backend.
  2. The scalar implementation lets the source build without a SIMD backend.
  3. A verified target backend can use the instruction sequences listed by code-generation checks.

The library does not promise a portable vector representation or calling convention. A compiler switch, compiler version, architecture, or enabled instruction set can change the private representation.

Separate scalar results from target mechanisms

PackagePurpose
Flyology_SIMDPublic 128-bit types, operations, contracts, and scalar implementation for all ten value families.
Backends.NativeThe full 128-bit primitive contract for the selected scalar, AArch64, or x86-64 body.
Backends.ScalarThe full 128-bit primitive contract. Each declaration renames the corresponding root scalar subprogram and matches a Backends.Native declaration.
Flyology_SIMD.WidePrivate 256-bit types, the initial Wide operation profile, and its scalar authority.
Flyology_SIMD.Wide.NativeStatic target selection for Wide operations. Most operations compose selected 128-bit operations across private parts. The optional x86-64 AVX2 backend supplies isolated 256-bit implementations for selected byte operations, floating arithmetic, table lookup, lane movement, and both Permute_Lanes overloads.
Algorithms.Generic_BytesStatic composition for Find_First, Find_First_Of, Find_First_Difference, Equal, Count, Count_In_Range, Add_Saturate, and Is_ASCII.
Algorithms.Generic_FloatingStatic composition for binary32 and binary64 Scale, Clamp, AXPY, Sum, Min_Number, Max_Number, and Dot_Product complete-array loops.
Algorithms.RuntimeOne backend choice for one complete floating-array or byte-buffer operation.

The root scalar body is the semantic reference for the complete 128-bit family. The Wide body is the scalar authority for its initial profile. Differential tests compare every current Wide Native operation group with that authority.

Wide values currently contain two private 128-bit parts. The two-part representation is private and does not define an ABI. The optional x86-64 AVX2 backend has 256-bit instruction-sequence claims for selected U8x32 and I8x32 operations, F32x8 and F64x4 arithmetic, lane movement, and both Permute_Lanes overloads.

Wide.Native uses these mechanisms:

  • Bit casts apply the selected 128-bit bit cast to each private part.
  • The lane-movement operations and both Permute_Lanes overloads use a target-selected permutation mechanism.
    • The AArch64 backend uses a two-register tbl table for reverse, slides, and the one-source Permute_Lanes overload. It uses a four-register table for interleave, deinterleave, and the two-source Permute_Lanes overload.
    • On composed x86-64, reverse and the one-source overload call selected 128-bit two-source Permute_Lanes twice. Interleave, deinterleave, and the two-source overload call selected 128-bit two-source Permute_Lanes four times and selected Select_Value twice to choose between the two sources. Slides call selected 128-bit two-source Permute_Lanes twice and selected Select_Value twice against Zero.
    • The optional AVX2 implementation uses 256-bit byte shuffles, cross-half selection, and a 32-byte index map for each operation.
  • Conversions compose selected 128-bit operations. Widening applies Widen_Low and Widen_High to the selected private part. Narrowing converts both private parts of each input. Same-width numeric conversions apply one operation to each private part.
  • All 80 Wide memory overloads compose selected 128-bit operations. Full, unaligned, and aligned forms call the matching operation for both private parts. Partial loads and stores choose selected full and partial operations from Count. When Count does not exceed the private lane count, a partial load uses selected Zero for the high part. A scalar build uses the same composition through the portable 128-bit implementation.
  • The composed and AArch64 byte mechanisms apply selected 128-bit operations to both private parts. The AVX2 mechanism uses isolated 256-bit subprograms for the Add_Wrap, Subtract_Wrap, Multiply_Wrap, Add_Saturate, Subtract_Saturate, Bitwise_And, Bitwise_Or, Bitwise_Xor, Bitwise_Not, Min, Max, Equal, Less_Than, Less_Equal, Greater_Than, Greater_Equal, and Select_Value byte overloads. Each byte comparison and Select_Value uses a relation-specific isolated leaf. The Less_Than leaf reverses the operands of its greater-than comparison. The Less_Equal leaf complements that comparison with the original operand order. The Greater_Equal leaf complements it with reversed operands.
  • The F32x8 and F64x4 Add, Subtract, Multiply, Divide, Min_Number, and Max_Number overloads use two selected 128-bit operations on AArch64 and the composed x86-64 backend. The optional AVX2 mechanism uses one isolated 256-bit leaf and vzeroupper. Arithmetic uses one packed instruction. Minimum and maximum use integer classification and bit selection to preserve the public NaN and signed-zero rules.
  • The 32-entry Table_Lookup uses a target-selected mechanism. AArch64 runs one two-register tbl operation for each result half. The composed x86-64 backend and a scalar build use one selected 128-bit Splat, four selected 128-bit Table_Lookup operations, two selected 128-bit Subtract_Wrap operations, and two selected 128-bit Bitwise_Or operations. An x86-64 build can select one isolated AVX2 256-bit implementation instead.
  • Compress and Expand use the target-selected compression and expansion mechanism for all ten Wide value types. On AArch64, the mechanism applies selected 128-bit To_Bit_Mask to both private mask parts. It combines the two compact results and derives one 32-byte index map. It runs one two-register tbl operation for each 128-bit result half. The x86-64 composed and AVX2 mechanisms each derive one two-source lane map for each 128-bit result half. Each mechanism calls selected 128-bit Permute_Lanes twice and selected 128-bit Select_Value twice. Select_Value selects Zero for each zero-fill lane.
  • The Horizontal_Sum operation computes the exact byte sum by adding two target-selected 128-bit exact byte-sum results.
  • The integer Reduce_Add_Wrap, Reduce_Min, and Reduce_Max overloads reduce both private parts with selected 128-bit reductions. The implementation splats each scalar result, combines the two vectors with selected 128-bit Add_Wrap, Min, or Max, and extracts lane 0. This grouping applies only to associative integer reductions.
  • The floating Reduce_Add, Reduce_Min_Number, and Reduce_Max_Number overloads combine lanes in ascending lane order and do not reduce the private parts independently. Reduce_Add starts from positive zero. The minimum-number and maximum-number reductions start from lane 0. On AArch64, dedicated Advanced SIMD sequences perform scalar fadd, fminnm, or fmaxnm operations in ascending lane order. The x86-64 composed and AVX2 selections use dedicated SSE2 leaves that process lanes in ascending order. A scalar build uses the portable Wide implementation.

The scalar and Native tests compare these mechanisms with the Wide scalar authority.

Select primitive code at build time

The GPR scenario selects one body for Backends.Native. The choices are scalar, aarch64, and x86_64. Native architecture detection sets this value for normal Alire builds.

FLYOLOGY_SIMD_WIDE_BACKEND selects optional Wide mechanisms. Its default value, composed, preserves the target's composed implementations. The value avx2 selects isolated 256-bit byte, floating-arithmetic, lookup, and permutation implementations. The build accepts this value only with FLYOLOGY_SIMD_ARCH=x86_64 and FLYOLOGY_SIMD_AVX2=enabled.

The Wide AVX2 selection requires compatible targets.

The static Wide operations perform no runtime feature check. Before a target runs this build, CPUID must report the AVX, AVX2, and OSXSAVE bits, and XCR0 must enable XMM and YMM register state.

Algorithms.Generic_Bytes receives primitive operations as generic formal subprograms. GNAT can inline those operations into the vector loop. There is no runtime feature test in each addition, comparison, or load.

Select optional code once per buffer

Features reports which backends were compiled and which can run on the current system. Algorithms.Runtime chooses one implementation before it starts a complete-array or whole-buffer loop.

The feature-checked Algorithms.AVX2 boundary is separate from the statically selected Wide operations. Selecting the Wide AVX2 backend does not add runtime dispatch.

On x86-64, the AVX2 check requires CPUID to report the AVX, AVX2, and OSXSAVE bits. It also requires XCR0 to enable XMM and YMM register state. The AVX2 implementation is in separate objects.

AArch64 Advanced SIMD, commonly called NEON, is part of the AArch64 architecture baseline. The AArch64 backend does not need a separate NEON feature test.

Keep target code in small Ada leaves

The project first uses a verified compiler intrinsic when GNAT exposes one with the required semantics. If no suitable intrinsic exists, it uses a small Ada System.Machine_Code subprogram.

These subprograms contain only the target mechanism. Ada code implements extent checks, oversized-shift behavior, and the other public semantics. Differential tests check the result. Code-generation scripts check the emitted instruction class.

A primitive operation does not allocate memory, start a task, perform I/O, lock, wait, or read process configuration.

Add a backend

  1. Implement the existing Backends.Native declarations in a target source directory.
  2. Keep value and mask representations private.
  3. Match every edge case in the scalar implementation.
  4. Run deterministic differential tests for every implemented operation.
  5. Run exact-tail and protected-page tests.
  6. Inspect the listed primitive operations and complete loops in generated code.
  7. Record compile, execution, and continuous-test evidence separately.

Do not add a public operation only because one processor has an instruction. Add it when its semantics are useful on every backend.