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:

  1. PDL (Perl Data Language) The standard for high-performance numerical computing in Perl. It is heavily optimized for processing large arrays ("piddles") in C.

  2. 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):

  1. Micro-Benchmark: Call a function many times (measuring call overhead).
  2. 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

  1. 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::C compiles 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.

  2. Macro (Throughput): Virtual Tie In the bulk data test, Affix and Inline::C perform 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

Of course, none of these points matter if you can't write the C or Fortran to emulate the features of PDL.