Choose backends at an algorithm boundary

Chapter 6 composes primitive operations statically and dispatches complete floating and byte algorithms at runtime.

A primitive operation must not test CPU features on every call. Two composition choices avoid that cost:

  • Bind a complete loop to known operations.
  • Before a supported runtime algorithm processes an array or buffer, select one implementation.

Pass operations to a generic algorithm

Algorithms.Generic_Floating supplies binary32 and binary64 scaling, clamping, AXPY, sums, number extrema, and dot products. Its formal operations include zero, splat, partial load and store, extract, multiply, add, minimum-number, maximum-number, and their reductions.

The excerpt below shows the binary32 formals. They operate on F32x4 values and F32_Array inputs. A Lane_Count_32x4 declares each partial extent, and the reduction returns F32. The package has matching binary64 formals and an F64_Array overload.

generic
   with function Backend_F32_Zero return F32x4;
   with function Backend_F32_Load_Partial
     (Data : F32_Array; Start : Natural;
      Count : Lane_Count_32x4) return F32x4;
   with procedure Backend_F32_Store_Partial
     (Data : in out F32_Array; Start : Natural;
      Count : Lane_Count_32x4; Value : F32x4);
   with function Backend_F32_Splat (Value : F32) return F32x4;
   with function Backend_F32_Multiply (Left, Right : F32x4) return F32x4;
   with function Backend_F32_Add (Left, Right : F32x4) return F32x4;
   with function Backend_F32_Min_Number (Left, Right : F32x4) return F32x4;
   with function Backend_F32_Max_Number (Left, Right : F32x4) return F32x4;
   with function Backend_F32_Reduce_Add (Value : F32x4) return F32;
package Flyology_SIMD.Algorithms.Generic_Floating is
   procedure Scale (Data : in out F32_Array; Factor : F32);
   procedure Clamp (Data : in out F32_Array; Low, High : F32);
   procedure AXPY (Y : in out F32_Array; A : F32; X : F32_Array);
   function Sum (Data : F32_Array) return F32;
   function Min_Number (Data : F32_Array) return F32;
   function Max_Number (Data : F32_Array) return F32;
   function Dot_Product (Left, Right : F32_Array) return F32;
   --  Matching binary64 formals and the F64_Array overload follow.
end Flyology_SIMD.Algorithms.Generic_Floating;

The generic body contains no feature test or indirect call for each vector. The compiler resolves each formal operation during instantiation.

Use the supplied static instances

The root Flyology_SIMD primitive bodies are the scalar reference. Backends.Scalar exposes that primitive contract for generic composition. Backends.Native exposes the matching contract with the body selected for the build target.

Algorithms.Scalar_Floating binds the generic to the scalar primitives. Algorithms.Native_Floating binds it to the selected native primitives.

Scalar : constant F32 :=
  Algorithms.Scalar_Floating.Dot_Product (Left, Right);
Native : constant F32 :=
  Algorithms.Native_Floating.Dot_Product (Left, Right);

These instances provide compile-time composition. Static composition does not by itself promise that GNAT inlines every primitive call.

The maintained dot_product.adb example runs the ordinary Ada loop, both static instances, and runtime dispatch. All four return 70.0 for its fixed inputs.

Let Alire select a native host backend

Alire selects AArch64 or x86-64 for a native host build. The GPR default remains scalar for an unknown target or a build outside Alire.

Use FLYOLOGY_SIMD_ARCH=scalar for a forced scalar build. Use explicit architecture values for supported cross-compilation workflows.

Dispatch once at a complete-array boundary

The library provides runtime dispatch for complete scaling, clamping, AXPY, sums, number extrema, and dot products, not for primitive floating operations.

Algorithms.Runtime selects one implementation before a supported complete-array loop. Primitive operations remain statically bound and perform no feature test.

Result : constant F32 :=
  Flyology_SIMD.Algorithms.Runtime.Dot_Product (Left, Right);

Scale, Clamp, AXPY, Sum, Min_Number, Max_Number, and Dot_Product have binary32 and binary64 overloads. Scale multiplies every element in place. Clamp applies Min_Number (Max_Number (Element, Low), High), so NaNs, signed zeros, and inverted bounds follow those exact primitive semantics. AXPY applies Y := A * X + Y with separate multiplication and addition and requires matching array bounds. Complete-array number extrema require nonempty input and preserve the primitive NaN and signed-zero rules. Dot_Product also requires its arrays to have matching bounds. Binary32 reductions use four accumulator lane groups; binary64 reductions use two. Sum and dot-product groups reduce in ascending lane order, and empty sums return positive zero.

Flyology_SIMD.Algorithms.Runtime.Scale
  (Measurements, Factor => 1.5);
Flyology_SIMD.Algorithms.Runtime.Clamp
  (Measurements, Low => 0.0, High => 100.0);
Flyology_SIMD.Algorithms.Runtime.AXPY
  (Y => Measurements, A => 0.5, X => Corrections);
Minimum : constant F32 :=
  Flyology_SIMD.Algorithms.Runtime.Min_Number (Measurements);

An AVX2 scaling selection broadcasts the factor once, then loads, multiplies, and stores eight binary32 or four binary64 elements at a time. The AVX2 AXPY loop broadcasts A, performs separate vector multiply and add operations, and stores Y. AVX2 sum and dot-product reductions add the two 128-bit halves to one accumulator in source order. The AVX2 clamp and complete-array number-extrema facades reuse the exact selected 128-bit minimum-number and maximum-number loops. These routes preserve the public semantics of the other runtime backends.

The runtime package also dispatches Find_First, Find_First_Of, Find_First_Difference, Equal, Count, Count_In_Range, Add_Saturate, and Is_ASCII at whole-buffer boundaries.

Matches : constant Natural :=
  Flyology_SIMD.Algorithms.Runtime.Count (Data, Comma);

The runtime package selects one safe implementation before it scans the complete byte array. No feature test runs for each vector operation.

Count_In_Range counts an inclusive unsigned byte interval. Inverted bounds describe an empty interval and return zero. The maintained count_digits.adb example uses one complete-buffer call for the interval '0' .. '9'.

Add_Saturate adds one byte value to every buffer element in place and clamps each result to 255. The AVX2 route broadcasts the addend and applies vpaddusb to 32 bytes at a time before its exact scalar tail.

Flyology_SIMD.Algorithms.Runtime.Add_Saturate
  (Pixels, Value => 24);

Find_First_Of uses the same whole-buffer boundary for an exact byte set. Empty data or an empty set returns (Found => False, Index => 0). For sets of one through four bytes, selected optimized backends keep load, comparison, mask extraction, and first-match selection in one loop. Larger sets retain exact semantics through the scalar membership path.

Separators : constant Byte_Array := [9, 10, 13, 32];
First      : constant Algorithms.Search_Result :=
  Flyology_SIMD.Algorithms.Runtime.Find_First_Of
    (Data, Separators);

Find_First_Difference and Equal compare arrays with identical Ada bounds. The first operation returns the lowest differing index, while equal arrays return (Found => False, Index => 0). The Boolean operation is true exactly in that no-difference case.

First_Change : constant Algorithms.Search_Result :=
  Flyology_SIMD.Algorithms.Runtime.Find_First_Difference
    (Previous, Current);

The AArch64 small-set scan journal records the representation decision, generated-code checks, and measured crossover on one machine.

Distinguish compiled, available, and best available

  • Compiled means that the build contains the backend code.
  • Available means that the current processor and operating system can execute it.
  • Best available identifies the preferred safe implementation among the compiled choices.

Features.Compiled, Features.Available, and Features.Best_Available report these states.

A forced runtime call raises Backend_Unavailable before it enters code that the host cannot execute.

Keep optional x86 instructions in separate objects

The x86-64 baseline uses SSE2 and compiles with AVX disabled. Optional AVX2 byte algorithms are in separate objects.

AVX2 availability requires processor AVX and AVX2 bits, OSXSAVE, and XCR0 support for XMM and YMM state. Checking only the AVX2 bit is not sufficient.

AArch64 Advanced SIMD is part of the AArch64 baseline, so NEON does not need an equivalent runtime gate.