So, you read Chapters 22 and 23, but the ultimate question remains: "Is it fast?"
To find out, we'll benchmark Affix against the two titans of Perl performance:
-
PDL (Perl Data Language) The standard for high-performance numerical computing in Perl. It is heavily optimized for processing large arrays ("piddles") in C.
-
Inline::C The traditional way to write C extensions. It compiles C code on the fly and links it to Perl via XS. It is generally considered the speed limit of Perl.
Benchmark 1: SIMD Vector Addition
This set of benchmarks tests using 128-bit SIMD vectors (adding four floats at once):
- Micro-Benchmark: Call a function many times (measuring call overhead).
- Macro-Benchmark: Call a function once to process 100,000 items (measuring raw throughput).
use v5.40;
use Benchmark qw[cmpthese];
use Affix qw[:all];
use Affix::Compiler;
use PDL;
use Inline C => Config => ( CCFLAGSEX => '-O3 -mavx' );
use Inline C => <<~'END_XS';
typedef float v4f __attribute__((vector_size(16)));
// Micro: Add single vector
SV* inline_add(SV* a_sv, SV* b_sv) {
STRLEN len_a, len_b;
float* a = (float*)SvPVbyte(a_sv, len_a);
float* b = (float*)SvPVbyte(b_sv, len_b);
if (len_a < 16 || len_b < 16) croak("Bad length");
v4f vc = *(v4f*)a + *(v4f*)b;
return newSVpvn((const char*)&vc, 16);
}
// Macro: Add arrays of vectors
void inline_bulk(SV* a_sv, SV* b_sv, SV* out_sv, int count) {
v4f* a = (v4f*)SvPV_nolen(a_sv);
v4f* b = (v4f*)SvPV_nolen(b_sv);
SvUPGRADE(out_sv, SVt_PV);
SvGROW(out_sv, count * 16 + 1);
SvCUR_set(out_sv, count * 16);
v4f* out = (v4f*)SvPV_nolen(out_sv);
for(int i=0; i<count; i++) {
out[i] = a[i] + b[i];
}
}
END_XS
# Compile C Library for Affix
my $c = Affix::Compiler->new( flags => { cflags => '-O3 -mavx' } );
$c->add( \<<~'END', lang => 'c' );
typedef float v4f __attribute__((vector_size(16)));
v4f affix_add(v4f a, v4f b) {
return a + b;
}
void affix_bulk(v4f *a, v4f *b, v4f *out, int count) {
for(int i=0; i<count; i++) {
out[i] = a[i] + b[i];
}
}
END
my $lib = $c->link;
# Standard (Safe) Binding
my $std_add = wrap $lib, 'affix_add', [ Vector [ 4, Float ], Vector [ 4, Float ] ] => Vector [ 4, Float ];
my $std_bulk = wrap $lib, 'affix_bulk', [ Pointer [Void], Pointer [Void], Pointer [Void], Int ] => Void;
# Setup Data
# A = [1, 2, 3, 4], B = [5, 6, 7, 8] -> Sum = [6, 8, 10, 12]
my $v1 = pack( 'f4', 1, 2, 3, 4 );
my $v2 = pack( 'f4', 5, 6, 7, 8 );
my $p1 = pdl( [ 1, 2, 3, 4 ] );
my $p2 = pdl( [ 5, 6, 7, 8 ] );
my $count = 100_000;
my $bytes = $count * 16;
my $big_a = $v1 x $count;
my $big_b = $v2 x $count;
my $big_out = "\0" x $bytes;
my $big_p1 = $p1->dummy( 1, $count )->clump(2);
my $big_p2 = $p2->dummy( 1, $count )->clump(2);
# Helper to unpack Vector result if Affix returns ArrayRef
sub unpack_if_ref($val) {
return ref($val) eq 'ARRAY' ? pack( 'f4', @$val ) : $val;
}
# Test 1: Micro-Benchmark
say 'Micro-Benchmark (Single Vector Add)';
cmpthese(
-5,
{ PDL => sub { my $r = $p1 + $p2; },
'Inline::C' => sub { inline_add( $v1, $v2 ) },
Affix => sub { $std_add->( $v1, $v2 ) }
}
);
# Test 2: Macro-Benchmark
say 'Macro-Benchmark (100k Vectors / 1.6MB)';
cmpthese(
-5,
{ PDL => sub { my $r = $big_p1 + $big_p2; },
'Inline::C' => sub { inline_bulk( $big_a, $big_b, $big_out, $count ) },
Affix => sub { $std_bulk->( \$big_a, \$big_b, \$big_out, $count ) }
}
);
The Results
Micro-Benchmark (Single Vector Add)
Rate PDL Inline::C Affix
PDL 623532/s -- -92% -92%
Inline::C 7706580/s 1136% -- -3%
Affix 7933356/s 1172% 3% --
Macro-Benchmark (100k Vectors / 1.6MB)
Rate PDL Inline::C Affix
PDL 1436/s -- -94% -94%
Inline::C 23291/s 1522% -- -1%
Affix 23473/s 1535% 1% --
Analysis
-
Micro (Latency): Affix Wins by a Nose Affix is 3% faster than compiled XS (
Inline::C) and more than 10x faster than PDL for small operations. This result is remarkable.Inline::Ccompiles static C code, but the XS macro system (SvPV,newSV) adds overhead. Affix generates a custom JIT trampoline at runtime that accesses the Perl stack and CPU registers directly, effectively behaving like inline assembly for Perl. -
Macro (Throughput): Virtual Tie In the bulk data test, Affix and
Inline::Cperform identically. This confirms that Affix has zero overhead once the pointer is passed to C.
Benchmark 2: Matrix Math (PDL Logic)
PDL is famous for its terse syntax and "broadcasting" capabilities. A classic example from the PDL synopsis is filling a matrix based on its coordinates and summing the results.
Before we race, we will verify that our C implementation produces the exact same results as PDL.
The Logic:
$y = $x + 0.1 * xvals($x) + 0.01 * yvals($x)
use v5.40;
use Benchmark qw[cmpthese];
use Affix qw[:all];
use Affix::Compiler;
use PDL;
# 1. Compile C Matrix Logic
my $c = Affix::Compiler->new( flags => { cflags => '-O3' } );
$c->add( \<<~'END', lang => 'c' );
void calc_matrix(double *out, int rows, int cols) {
for(int y=0; y < rows; y++) {
for(int x=0; x < cols; x++) {
int i = y * cols + x;
// logic: 0 + 0.1*x + 0.01*y
out[i] = 0.1 * x + 0.01 * y;
}
}
}
END
my $mat_lib = $c->link;
my $calc = wrap $mat_lib, 'calc_matrix', [ Pointer [Void], Int, Int ] => Void;
# 2. Setup (1000x1000 Matrix)
my $N = 1000;
my $p_mat = zeroes $N, $N;
my $c_buf = "\0" x ( $N * $N * 8 ); # 8 bytes per double
# 3. Verification
say "\nVerifying results...";
# Run PDL Logic
my $p_res = $p_mat + 0.1 * xvals($p_mat) + 0.01 * yvals($p_mat);
# Run Affix Logic
$calc->( \$c_buf, $N, $N );
# Compare value at specific coordinate [500, 500]
# Expected: 0.1*500 + 0.01*500 = 50 + 5 = 55
my $idx = 500 * $N + 500;
my $val_c = unpack( 'd', substr( $c_buf, $idx * 8, 8 ) );
my $val_pdl = $p_res->at( 500, 500 );
if ( abs( $val_c - $val_pdl ) < 1e-9 ) {
say "Verification Successful: Matches at [500, 500] ($val_c)";
}
else {
die "Mismatch! C=$val_c PDL=$val_pdl";
}
# 4. Benchmark
say "Matrix Benchmark (1000x1000)";
cmpthese(
-5,
{ PDL => sub {
my $y = $p_mat + 0.1 * xvals($p_mat) + 0.01 * yvals($p_mat);
},
Affix => sub {
# The raw C logic via Affix
$calc->( \$c_buf, $N, $N );
}
}
);
The Results
Verifying results...
Verification Successful: Matches at [500, 500] (55)
Matrix Benchmark (1000x1000)
Rate PDL Affix
PDL 78.5/s -- -99%
Affix 6795/s 8555% --
Analysis
In this specific case, Affix is nearly 9x faster than PDL.
While PDL is highly optimized C, it still has to allocate temporary objects for xvals, yvals, and the intermediate multiplication results before summing them. The custom C function loaded by Affix does the calculation in a single pass over the memory with no intermediate allocations, allowing the CPU to cache lines efficiently.
Summary
- Speed: Affix is consistently the fastest way to bridge Perl and C, beating XS in call overhead and beating generic numerical libraries in raw throughput via custom C implementations.
- Simplicity: We achieved this performance with one line of Perl (
affix), versus lines of XS macros or complex PDL threading logic. - Portability: Affix requires no compilation at install time. You can ship your script without a C compiler dependency (if binding to pre-compiled libraries) and still get maximum performance.
Of course, none of these points matter if you can't write the C or Fortran to emulate the features of PDL.