Compute and rearrange lanes explicitly

Chapter 2 completes a four-lane dot product, computes an eight-lane weighted sum, and then moves lanes within and across vector blocks.

Chapter 1 formed four products. This chapter completes the dot product, then shows how slides and lane maps rearrange data for other calculations.

Accumulate four lane totals

The maintained example passes scalar or native operations to one generic function. Its loop has this structure:

Accumulator : F32x4 := Vector_Zero;

while Start <= Left'Last loop
   --  Count and partial loads are shown in Chapter 4.
   Accumulator := Vector_Add
     (Accumulator, Vector_Multiply (Left_Block, Right_Block));
   exit when Count = Remaining;
   Start := Start + Count;
end loop;

return Vector_Reduce_Add (Accumulator);

Add and Multiply operate on corresponding F32x4 lanes. They do not implicitly reduce the result.

Reduce lanes in a defined order

Reduce_Add starts with positive zero and adds lanes in ascending order. The fold is ((((0.0 + lane 0) + lane 1) + lane 2) + lane 3) on every backend.

The dot-product loop accumulates element indexes 1 and 5 in lane 0, indexes 2 and 6 in lane 1, and so on. For the maintained input, the accumulator is [13.0, 20.0, 29.0, 8.0]. The defined fold returns 70.0.

Do not require bitwise equality with the ordinary loop for arbitrary floating inputs.

Floating-point addition is not associative. The ordinary loop and vector loop may round at different points. The maintained example uses exactly representable products and sums, so both return 70.0.

Run the complete dot-product example

alr exec -- gprbuild -p -P examples/examples.gpr
./bin/dot_product
ordinary Ada dot: 7.00000E+01
scalar backend dot: 7.00000E+01
native backend dot: 7.00000E+01

Open the maintained source. Chapter 4 explains why the three-element tail is safe.

Compose an eight-lane weighted sum

A weighted sum is a dot product whose second vector contains weights. It computes sum (Samples (i) * Weights (i)). The Flyology_SIMD.Wide package defines the private F32x8 type, which contains eight binary32 lanes. Flyology_SIMD.Wide.Native supplies statically selected operations for this type.

The example loads two eight-element F32_Array values, multiplies corresponding lanes, and reduces the eight products:

package Wide renames Flyology_SIMD.Wide;
package Native renames Flyology_SIMD.Wide.Native;

Samples : constant F32_Array :=
  [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
Weights : constant F32_Array :=
  [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0];

Sample_Vector : constant Wide.F32x8 :=
  Native.Load_Unaligned (Samples, Samples'First);
Weight_Vector : constant Wide.F32x8 :=
  Native.Load_Unaligned (Weights, Weights'First);
Products : constant Wide.F32x8 :=
  Native.Multiply (Sample_Vector, Weight_Vector);
Product_Lanes : constant Wide.Lane_Values_F32x8 :=
  Native.To_Lanes (Products);
Result : constant F32 := Native.Reduce_Add (Products);
Smallest_Product : constant F32 := Native.Reduce_Min_Number (Products);
Largest_Product : constant F32 := Native.Reduce_Max_Number (Products);

The two Load_Unaligned calls form the vectors. Multiply calculates eight products. To_Lanes returns a Lane_Values_F32x8 array for inspection. Reduce_Add, Reduce_Min_Number, and Reduce_Max_Number each return one F32 value.

The products are [2, 6, 15, 28, 55, 78, 119, 152]. The sum reduction starts with positive zero, adds lanes in ascending order, and returns 455. The number-minimum and number-maximum reductions start at lane 0 and return 2 and 152.

The From_Lanes overload can also expose why order matters:

Order_Sensitive : constant Wide.F32x8 := Native.From_Lanes
  ([1.0E20, 1.0, 0.0, 0.0, -1.0E20, 1.0, 0.0, 0.0]);
Ordered_Result : constant F32 := Native.Reduce_Add (Order_Sensitive);

The ascending fold returns 1. The first addition of 1 is rounded away at the scale of 1.0E20. The later -1.0E20 cancels the large partial sum before the second 1 is added. Reducing the two private halves independently would change that grouping and can change rounding, NaN, and signed-zero results.

alr exec -- gprbuild -p -P examples/examples.gpr
./bin/wide_dot_product
products: 2.00000E+00 6.00000E+00 1.50000E+01 2.80000E+01 5.50000E+01 7.80000E+01 1.19000E+02 1.52000E+02
weighted sum: 4.55000E+02
product range: 2.00000E+00 .. 1.52000E+02
order-sensitive sum: 1.00000E+00

Open the maintained Wide example. The current implementation stores each Wide value as two private 128-bit parts. The two-part representation is private and does not define an ABI. Loads and multiplication compose selected 128-bit operations. On AArch64 and x86-64, each floating reduction uses a dedicated target sequence that preserves the ascending fold. A scalar build uses the portable Wide implementation.

Slide lanes to use neighboring values

A three-point stencil combines each sample with its available left and right neighbors. This example keeps the Wide and Native aliases from the weighted sum. The From_Lanes overload that returns F32x8 constructs eight samples.

Samples : constant Wide.F32x8 :=
  Native.From_Lanes ([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
Previous : constant Wide.F32x8 :=
  Native.Slide_Lanes_Toward_High (Samples, 1);
--  [0, 1, 2, 3, 4, 5, 6, 7]
Following : constant Wide.F32x8 :=
  Native.Slide_Lanes_Toward_Low (Samples, 1);
--  [2, 3, 4, 5, 6, 7, 8, 0]
Sums : constant Wide.F32x8 :=
  Native.Add (Native.Add (Previous, Samples), Following);

--  Sums is [3, 6, 9, 12, 15, 18, 21, 15].

Slide_Lanes_Toward_High inserts positive zero at lane 0 and moves 4.0 from lane 3 to lane 4. Slide_Lanes_Toward_Low inserts positive zero at lane 7 and moves 5.0 from lane 4 to lane 3. Lanes 3 and 4 are in different private 128-bit parts, but this implementation boundary does not change the public lane rule. The two Add calls combine each sample with the shifted neighbors.

Build the examples, and then run the maintained stencil:

alr exec -- gprbuild -p -P examples/examples.gpr
./bin/lane_slides
toward high lanes 0..3: 0.00000E+00,  1.00000E+00,  2.00000E+00,  3.00000E+00
toward high lanes 4..7: 4.00000E+00,  5.00000E+00,  6.00000E+00,  7.00000E+00
toward low lanes 0..3: 2.00000E+00,  3.00000E+00,  4.00000E+00,  5.00000E+00
toward low lanes 4..7: 6.00000E+00,  7.00000E+00,  8.00000E+00,  0.00000E+00
sums lanes 0..3: 3.00000E+00,  6.00000E+00,  9.00000E+00,  1.20000E+01
sums lanes 4..7: 1.50000E+01,  1.80000E+01,  2.10000E+01,  1.50000E+01

The maintained lane_slides.adb example asserts and prints both intermediate vectors and the result. The operation reference defines zero, oversized, and edge-count behavior.

Reuse a lane map to rotate points

A lane slide moves all retained lanes by the same offset and fills the vacated lanes with zero. A reusable lane map selects each result lane independently from one source vector.

Store four planar points as [x0, y0, ..., x3, y3]. A 90-degree counterclockwise rotation maps each point (x, y) to (-y, x). The rotation first swaps each coordinate pair and then applies alternating signs.

A Lane_Selectors_32x8 value gives one source-lane index for each result lane. Wide.Make_Lane_Map constructs a reusable Lane_Map_32x8.

The Wide.Native.From_Lanes overload constructs each eight-lane input:

package Wide renames Flyology_SIMD.Wide;
package Native renames Flyology_SIMD.Wide.Native;

Swap_XY : constant Wide.Lane_Map_32x8 :=
  Wide.Make_Lane_Map ([1, 0, 3, 2, 5, 4, 7, 6]);

Points : constant Wide.F32x8 :=
  Native.From_Lanes
    ([2.0, 5.0, -3.0, 4.0, 0.0, -2.0, 7.0, 1.0]);
Signs : constant Wide.F32x8 :=
  Native.From_Lanes
    ([-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);

Swapped : constant Wide.F32x8 :=
  Native.Permute_Lanes (Points, Swap_XY);
Rotated : constant Wide.F32x8 :=
  Native.Multiply (Swapped, Signs);

--  The permutation is [5, 2, 4, -3, -2, 0, 1, 7].
--  Rotated is [-5, 2, -4, -3, 2, 0, -1, 7].

Selectors 1 and 0 supply the first result pair. Each later selector pair performs the same swap. The Wide.Native.Permute_Lanes call returns all four swapped points. Wide.Native.Multiply applies the signs and returns all four rotated points.

A repeated selector broadcasts one source lane to several result lanes. Permute_Lanes preserves each selected lane's complete bit pattern; it does not perform numeric conversion. The same 32x8 map can be reused with signed, unsigned, or floating 32-bit vectors.

This overload of Permute_Lanes reads one source vector. The next example uses a separate overload when the result needs lanes from two vectors.

alr exec -- gprbuild -p -P examples/examples.gpr
./bin/permute_points
rotated points:
  point 0: x=-5.00000E+00, y= 2.00000E+00
  point 1: x=-4.00000E+00, y=-3.00000E+00
  point 2: x= 2.00000E+00, y= 0.00000E+00
  point 3: x=-1.00000E+00, y= 7.00000E+00

The maintained point-rotation example creates the map once and asserts both the swapped lanes and rotated points.

Select successors across a block boundary

A first difference subtracts each sample from its successor. This example uses the same Wide and Native aliases as the point rotation. Two adjacent F32x8 values hold sixteen consecutive square numbers. The successor for left lane 7 is in right lane 0.

Wide.Select_Left_Lane and Wide.Select_Right_Lane return the private Two_Source_Lane_Selector_32x8 type. Each selector records a source and a lane index. Wide.Make_Two_Source_Lane_Map stores eight selectors in a reusable Two_Source_Lane_Map_32x8.

Successor_Map : constant Wide.Two_Source_Lane_Map_32x8 :=
  Wide.Make_Two_Source_Lane_Map
    ([Wide.Select_Left_Lane (1), Wide.Select_Left_Lane (2),
      Wide.Select_Left_Lane (3), Wide.Select_Left_Lane (4),
      Wide.Select_Left_Lane (5), Wide.Select_Left_Lane (6),
      Wide.Select_Left_Lane (7), Wide.Select_Right_Lane (0)]);

Left : constant Wide.F32x8 :=
  Native.From_Lanes
    ([1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0]);
Right : constant Wide.F32x8 :=
  Native.From_Lanes
    ([81.0, 100.0, 121.0, 144.0,
      169.0, 196.0, 225.0, 256.0]);

Successors : constant Wide.F32x8 :=
  Native.Permute_Lanes (Left, Right, Successor_Map);
Differences : constant Wide.F32x8 :=
  Native.Subtract (Successors, Left);

--  Successors is [4, 9, 16, 25, 36, 49, 64, 81].
--  Differences is [3, 5, 7, 9, 11, 13, 15, 17].

The three-argument Wide.Native.Permute_Lanes overload forms one continuous successor window. Wide.Native.Subtract then calculates eight first differences, including the difference across the vector boundary.

Selectors can repeat a lane from either source. Repetition broadcasts that lane to multiple result positions. The operation preserves each selected lane's complete bit pattern and does not perform numeric conversion. A default-initialized two-source map selects left lane 0 for every result lane.

Use a slide when every retained lane moves by the same offset and zero fills the edge. Use a one-source map for arbitrary selection within one vector. Use a two-source map when one result combines lanes from two vectors.

The same examples build contains the cross-block program. Run:

./bin/cross_block_differences
successors:
  lanes 0..3: 4.00000E+00, 9.00000E+00, 1.60000E+01, 2.50000E+01
  lanes 4..7: 3.60000E+01, 4.90000E+01, 6.40000E+01, 8.10000E+01
differences:
  lanes 0..3: 3.00000E+00, 5.00000E+00, 7.00000E+00, 9.00000E+00
  lanes 4..7: 1.10000E+01, 1.30000E+01, 1.50000E+01, 1.70000E+01

The maintained cross-block example asserts the intermediate and final vectors before it prints them.

Choose integer arithmetic and bitwise operations

Integer vector arithmetic does not use one implicit overflow policy. Add_Wrap computes modulo the lane width. Add_Saturate clamps addition to the lane range. Subtract_Saturate clamps subtraction to that range.

A U16x8 has eight unsigned 16-bit lanes. From_Lanes constructs the input, and Splat repeats the increment.

Input : constant U16x8 :=
  From_Lanes ([65_530, 1, 2, 3, 100, 200, 300, 400]);
Increment : constant U16x8 := Splat (10);

Added      : constant U16x8 := Add_Wrap (Input, Increment);
Subtracted : constant U16x8 := Subtract_Wrap (Input, Increment);
Multiplied : constant U16x8 := Multiply_Wrap (Input, Increment);
Saturated_Added : constant U16x8 := Add_Saturate (Input, Increment);
Saturated_Subtracted : constant U16x8 :=
  Subtract_Saturate (Input, Increment);

--  Added lane 0 = 4
--  Subtracted lane 1 = 65_527
--  Multiplied lane 0 = 65_476
--  Saturated_Added lane 0 = 65_535
--  Saturated_Subtracted lane 1 = 0

Subtract_Wrap and Multiply_Wrap use the same modulo-lane-width rule as Add_Wrap. All three wrapping forms discard bits above the lane width. By contrast, Subtract_Saturate clamps lane 1 to the unsigned lower limit instead of wrapping it to 65,527. The signed and unsigned families use the same explicit naming rule. The maintained integer_vectors example asserts these five arithmetic results before printing them.

Integer lanes also work as packed flag fields. Bitwise_And sets a result bit when that bit is set in both inputs. Bitwise_Or sets a result bit when that bit is set in either input. Bitwise_Xor sets a result bit when that bit is set in exactly one input. To clear selected bits, apply Bitwise_Not to the clear mask, then pass that result to Bitwise_And:

Flags : constant U16x8 :=
  From_Lanes ([16#ABF7#, 16#1234#, 16#00AA#, 16#FFFF#,
               16#8001#, 16#0102#, 16#5555#, 16#AAAA#]);

Known      : constant U16x8 := Bitwise_And (Flags, Splat (16#00FF#));
With_Ready : constant U16x8 := Bitwise_Or (Known, Splat (16#0100#));
Toggled    : constant U16x8 := Bitwise_Xor (With_Ready, Splat (16#0003#));
Cleared    : constant U16x8 :=
  Bitwise_And (Toggled, Bitwise_Not (Splat (16#0004#)));

--  Lane 0: ABF7 -> 00F7 -> 01F7 -> 01F4 -> 01F0.

The example independently asserts the masked, set, toggled, and cleared lane values.

Shift signed lanes with sign fill

Oversized logical shifts return zero lanes. An oversized signed arithmetic right shift returns the sign fill. The From_Lanes overload constructs the signed input below. The Shift_Right_Arithmetic overload preserves each I64x2 lane's sign:

Signed : constant I64x2 := From_Lanes ([-16, 16]);
By_Two : constant I64x2 := Shift_Right_Arithmetic (Signed, 2);
By_64  : constant I64x2 := Shift_Right_Arithmetic (Signed, 64);
--  By_Two = [-4, 4]; By_64 = [-1, 0]

Keep IEEE edge rules next to floating operations

The build does not enable -ffast-math. Ordered comparisons are false when either input lane is NaN. The Unordered result is true for that case.

Min_Number and Max_Number return the numeric input when only one quiet NaN is present. For two zeros, minimum selects negative zero and maximum selects positive zero.

Before an algorithm depends on NaN payloads, signaling state, signed zero, or conversion boundaries, read the semantic compatibility document.