Keep one Boolean result for each lane

Chapter 3 combines Wide byte comparisons in a classifier. It then uses floating masks for compression and expansion and byte masks for compact position bits.

A vector comparison returns a mask, not one Boolean. Each mask lane records the result for the corresponding value lane.

Compare corresponding lanes

Less_Than returns a private Mask_32x4. From_Lanes constructs the F32x4 products, and Zero supplies four positive-zero lanes.

Products : constant F32x4 :=
  From_Lanes ([-1.0, 2.0, -3.0, 4.0]);
Negative : constant Mask_32x4 :=
  Less_Than (Products, Zero);

The mask's Boolean results are [true, false, true, false]. Callers do not depend on an all-bits-set representation.

Select one value in each lane

Select_Value chooses its true argument in true mask lanes. Subtract negates the marked products by subtracting them from zero.

Magnitudes : constant F32x4 :=
  Select_Value (Negative, Subtract (Zero, Products), Products);

--  Magnitudes = [1.0, 2.0, 3.0, 4.0]

Mask values and value vectors are different private types. The API does not convert between them implicitly.

Classify 32 bytes with two comparisons

An ASCII decimal digit is a byte from '0' through '9', inclusive. The maintained example converts the 32-character text "sensor 17: row 204, sample 0091." to a Byte_Array named Data. Its helper converts each character position to a U8. The example renames Flyology_SIMD.Wide as Wide and Flyology_SIMD.Wide.Native as Native. It uses the private U8x32 and Mask_8x32 types.

Wide.Native.Load_Unaligned reads all 32 bytes. The exact Greater_Equal and Less_Equal overloads compare unsigned byte lanes. Splat repeats each boundary.

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

Values : constant Wide.U8x32 :=
  Native.Load_Unaligned (Data, Data'First);
At_Least_Zero : constant Wide.Mask_8x32 :=
  Native.Greater_Equal
    (Values, Native.Splat (U8 (Character'Pos ('0'))));
At_Most_Nine : constant Wide.Mask_8x32 :=
  Native.Less_Equal
    (Values, Native.Splat (U8 (Character'Pos ('9'))));
Is_Digit : constant Wide.Mask_8x32 :=
  Native.Mask_And (At_Least_Zero, At_Most_Nine);
Filtered : constant Wide.U8x32 :=
  Native.Select_Value
    (Is_Digit,
     Values,
     Native.Splat (U8 (Character'Pos ('.'))));
Digit_Count : constant Wide.Lane_Count_8x32 :=
  Native.Population_Count (Is_Digit);

Mask_And is true only where both range checks are true. Select_Value keeps each digit and puts a period in each other lane. Population_Count returns the number of digit lanes as a Lane_Count_8x32.

Build the maintained examples from the repository root, and then run the classifier:

alr exec -- gprbuild -p -P examples/examples.gpr
./bin/wide_digit_classifier

The program prints:

input : sensor 17: row 204, sample 0091.
digits: .......17......204.........0091.
count : 9

The maintained wide_digit_classifier.adb source converts the filtered vector to characters and asserts the count before it prints the result. The same program and result apply to the composed backend and the optional statically selected AVX2 backend.

Compress selected measurements

Compression keeps selected values in their source order. This example continues with the Wide and Native package renames from the classifier. It uses the private F32x8 type and its Mask_32x8 type. The F32x8 From_Lanes overload constructs eight measurements.

Greater_Than compares them with the F32x8 Zero value. This ordered comparison does not select a NaN lane.

Samples : constant Wide.F32x8 := Native.From_Lanes
  ([-2.5, 3.0, -1.0, 4.5, 6.25, -7.0, 8.5, 9.75]);
Keep : constant Wide.Mask_32x8 :=
  Native.Greater_Than (Samples, Native.Zero);
Packed : constant Wide.F32x8 := Native.Compress (Samples, Keep);
Expanded : constant Wide.F32x8 := Native.Expand (Packed, Keep);
Kept_Count : constant Wide.Lane_Count_32x8 :=
  Native.Population_Count (Keep);

The mask is [false, true, false, true, true, false, true, true]. It selects source lanes 1, 3, 4, 6, and 7. Their values are [3.0, 4.5, 6.25, 8.5, 9.75]. Lanes 3 and 4 are on opposite sides of the two private 128-bit parts. This implementation boundary does not change the public lane semantics.

Compress visits source lanes in order. It moves the five selected values to result lanes 0 through 4. It fills the remaining lanes with positive zero. The packed result is [3.0, 4.5, 6.25, 8.5, 9.75, 0.0, 0.0, 0.0].

Population_Count returns a Lane_Count_32x8. Here it returns 5, the number of selected lanes and the length of the packed prefix.

Expand consumes packed lanes 0 through 4. It restores their values to result lanes 1, 3, 4, 6, and 7. It fills false positions with positive zero. The result is [0.0, 3.0, 0.0, 4.5, 6.25, 0.0, 8.5, 9.75].

Expand (Compress (Samples, Keep), Keep) restores only the selected values. It cannot recover the three values that compression discarded. Both operations preserve all bits of each moved lane, including floating NaN payloads, infinities, and signed zeros.

Use the same examples build, and then run this program:

./bin/compact_measurements

The program prints:

input   :-2.50000E+00,  3.00000E+00, -1.00000E+00,  4.50000E+00,  6.25000E+00, -7.00000E+00,  8.50000E+00,  9.75000E+00
mask    :FALSE TRUE FALSE TRUE TRUE FALSE TRUE TRUE
count   : 5
packed  : 3.00000E+00,  4.50000E+00,  6.25000E+00,  8.50000E+00,  9.75000E+00,  0.00000E+00,  0.00000E+00,  0.00000E+00
expanded: 0.00000E+00,  3.00000E+00,  0.00000E+00,  4.50000E+00,  6.25000E+00,  0.00000E+00,  8.50000E+00,  9.75000E+00

The maintained compact_measurements.adb source asserts both result vectors and the count. It then prints the input lanes, mask, count, packed result, and expanded result.

Combine and reduce Boolean results

Mask_And, Mask_Or, Mask_Xor, and Mask_Not apply Boolean operations lane by lane.

Any_True, All_True, and None_True return one Boolean. Population_Count returns the number of true lanes.

Use compact masks for byte positions

A Byte_Array holds "red,green,blue,a". Load_Unaligned reads it into a U8x16. Splat repeats the comma value before Equal returns a Mask_8x16.

Input : constant Byte_Array :=
  [114, 101, 100, 44, 103, 114, 101, 101,
   110, 44, 98, 108, 117, 101, 44, 97];
Bytes : constant U8x16 := Load_Unaligned (Input, Input'First);
Matches : constant Mask_8x16 := Equal (Bytes, Splat (44));
Bits : constant Interfaces.Unsigned_16 := To_Bit_Mask (Matches);
Count : constant Lane_Count_8x16 := Population_Count (Matches);

To_Bit_Mask puts lane 0 in bit 0. Comma lanes 3, 9, and 14 produce 16#4208#. The Population_Count result is 3.

Mask_From_Bit_Mask performs the inverse mapping. It ignores bits above the lane count.

Check the no-match sentinel before conversion

First_True and Last_True return a lane-count value. They return 16 when no byte lane is true.

If the result is below Lane_Count_8x16'Last, convert the result to Lane_Index_8x16. A result equal to Lane_Count_8x16'Last is the no-match sentinel and must not be converted.