Standard Perl scalars are designed for flexibility, not raw math throughput. When you need to process millions of coordinates, pixels, or audio samples, you want SIMD (Single Instruction, Multiple Data).
Affix makes SIMD vectors (like __m128, __m256, float32x4_t) first-class citizens and properly manages support on both x84 and ARM64.
The Recipe
Let's write a hypothetical 3D math library that uses 128-bit vectors (4 floats).
use v5.40;
use Affix qw[:all];
use Affix::Build;
# 1. Compile C Library
# We use GCC/Clang vector extensions for this example.
my $c = Affix::Build->new( flags => { cflags => '-O3 -mavx' } );
$c->add( \<<~'END', lang => 'c', );
#if defined(__GNUC__) || defined(__clang__)
typedef float v4f __attribute__((vector_size(16)));
// Add two vectors
v4f add_vecs(v4f a, v4f b) {
return a + b;
}
// Scale a vector by a scalar
v4f scale_vec(v4f v, float s) {
return v * s;
}
// Dot product (returns scalar)
float dot_vec(v4f a, v4f b) {
v4f res = a * b;
return res[0] + res[1] + res[2] + res[3];
}
#endif
END
my $lib = $c->link;
# 2. Bind
# We define a generic "Vec4" type: v[4:float]
typedef Vec4 => Vector [ 4, Float ];
affix $lib, 'add_vecs', [ Vec4(), Vec4() ] => Vec4();
affix $lib, 'scale_vec', [ Vec4(), Float ] => Vec4();
affix $lib, 'dot_vec', [ Vec4(), Vec4() ] => Float;
# 3. Use (Packed Strings - The Fast Way)
# Pack 4 floats into a 16-byte string
my $v1 = pack( 'f4', 1.0, 2.0, 3.0, 4.0 );
my $v2 = pack( 'f4', 5.0, 6.0, 7.0, 8.0 );
# Add
my $sum_ref = add_vecs( $v1, $v2 );
# Results come back as ArrayRefs by default (for convenience)
say 'Sum: ' . join ', ', @$sum_ref; # 6, 8, 10, 12
# 4. Use (ArrayRefs - The Convenient Way)
# You can also pass ArrayRefs directly. Affix handles the packing.
my $scaled_ref = scale_vec( [ 1.0, 1.0, 1.0, 1.0 ], 0.5 );
say 'Scaled: ' . join( ', ', @$scaled_ref ); # 0.5, 0.5, 0.5, 0.5
# 5. Dot Product
my $dot = dot_vec( $v1, $v2 );
say 'Dot Product: ' . $dot; # 1*5 + 2*6 + 3*7 + 4*8 = 70
How It Works
-
1. First-Class Types
Vector[ 4, Float ]Affix understands that this is not just an array, but a specific machine type (128-bit register). On x64, this maps to
__m128and is passed in SSE registers (XMM0, etc.). On ARM64, it maps to NEON registers (V0, etc.). -
2. Packed String Input
my $v1 = pack( 'f4', ... );This is the most efficient way to work. You are creating the raw binary layout of the vector in a Perl scalar. Affix simply passes the pointer to this data directly to the C function (or loads it into a register). This avoids the overhead of iterating over a Perl array and converting individual values.
-
3. ArrayRef Output
By default, Affix unpacks returned vectors into a Perl ArrayRef (e.g.,
[x, y, z, w]). This is convenient for debugging or light math.
Kitchen Reminders
-
Alignment
SIMD instructions are picky about alignment. When you use
pack, Perl usually handles the string buffer allocation, but it doesn't guarantee 16-byte alignment. Affix handles the move to the register safely. If you usemallocto store vectors for C functions, be sure to use an aligned allocation if the C library requires it. -
Aliases
Affix exports aliases for common SIMD types if you import
:types:M128->Vector[4, Float]M128d->Vector[2, Double]M256->Vector[8, Float](AVX)