anr, anf: vectorize the LMS tap and update loops

Both loops indexed the delay line as (in_idx + j + delay) & mask, one
masked index per tap, which made the address non-affine and stopped the
vectorizer. The window wraps at most once, so split it at the wrap and
walk two contiguous runs instead.

As in resample.c, the y/sigma reduction cannot be reassociated without
-ffast-math (which this library must not enable, see linux_port.h), so
carry four independent accumulator pairs to break the FMA dependency
chain and let the vectorizer in.

in_buff and out_buff alias in RXA -- both are midbuff -- so only the
private d/w arrays are marked restrict.

Measured in situ on an Apple M1 Pro, 512-sample buffers, cost of turning
the block on, best of 5:

    anr    59303 ns -> 20253 ns   2.93x
    anf    55073 ns -> 20511 ns   2.68x

Summation order changes, so output is not bit-identical: over 300 buffers
of the full RX chain the worst deviation is 2.4e-07, an SNR of 153 dB.
An LMS filter is an adaptive feedback loop, so its trajectory is
chaotic. As a control, perturbing a single input sample of the unmodified
code by one ulp diverges it from itself by 6.6e-07, an SNR of 144.6 dB --
i.e. this change disturbs the filter less than the last bit of the input
does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-07-09 22:52:33 +03:00
co-authored by Claude Opus 4.8
parent 9960a1d0e3
commit 3faad14fc1
2 changed files with 188 additions and 54 deletions
+95 -28
View File
@@ -26,6 +26,45 @@ warren@wpratt.com
#include "comm.h"
/* Filter output and tap-window energy over a unit-stride run of the delay line.
The delay line is indexed (in_idx + j + delay) & mask, which wraps at most
once across the tap window; xanf() splits the window at the wrap so both
halves are contiguous here. Four independent accumulator pairs keep the FMAs
off a single dependency chain and let the vectorizer in -- a 'y += w[j]*x[j]'
reduction cannot be reassociated without -ffast-math, which this library must
not enable (it relies on IEEE semantics for 0/0 = NaN and x/0 = Inf). */
static inline void anf_dot (const double* WDSP_RESTRICT w,
const double* WDSP_RESTRICT x, int n, double* py, double* psigma)
{
double y0 = 0.0, y1 = 0.0, y2 = 0.0, y3 = 0.0;
double s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0;
int j = 0;
for (; j <= n - 4; j += 4)
{
y0 += w[j + 0] * x[j + 0]; s0 += x[j + 0] * x[j + 0];
y1 += w[j + 1] * x[j + 1]; s1 += x[j + 1] * x[j + 1];
y2 += w[j + 2] * x[j + 2]; s2 += x[j + 2] * x[j + 2];
y3 += w[j + 3] * x[j + 3]; s3 += x[j + 3] * x[j + 3];
}
for (; j < n; j++)
{
y0 += w[j] * x[j];
s0 += x[j] * x[j];
}
*py += (y0 + y1) + (y2 + y3);
*psigma += (s0 + s1) + (s2 + s3);
}
/* Leaky-LMS tap update over the same unit-stride run. */
static inline void anf_update (double* WDSP_RESTRICT w,
const double* WDSP_RESTRICT x, int n, double c0, double c1)
{
int j;
for (j = 0; j < n; j++)
w[j] = c0 * w[j] + c1 * x[j];
}
ANF create_anf (
int run,
int position,
@@ -81,53 +120,81 @@ void destroy_anf (ANF a)
void xanf(ANF a, int position)
{
int i, j, idx;
int i;
double c0, c1;
double y, error, sigma, inv_sigp;
double nel, nev;
if (a->run && (a->position == position))
{
for (i = 0; i < a->buff_size; i++)
{
a->d[a->in_idx] = a->in_buff[2 * i + 0];
const int n_taps = a->n_taps;
const int dline_size = a->dline_size;
const int mask = a->mask;
const int delay = a->delay;
const int buff_size = a->buff_size;
const double two_mu = a->two_mu;
const double gamma = a->gamma;
const double den_mult = a->den_mult;
const double lincr = a->lincr;
const double ldecr = a->ldecr;
const double lidx_min = a->lidx_min;
const double lidx_max = a->lidx_max;
/* in_buff and out_buff are the same buffer in RXA, so neither may be
marked restrict; d and w are private to the struct. */
const double* in_buff = a->in_buff;
double* out_buff = a->out_buff;
double* WDSP_RESTRICT d = a->d;
double* WDSP_RESTRICT w = a->w;
int in_idx = a->in_idx;
double lidx = a->lidx;
double ngamma = a->ngamma;
y = 0;
sigma = 0;
for (j = 0; j < a->n_taps; j++)
for (i = 0; i < buff_size; i++)
{
idx = (a->in_idx + j + a->delay) & a->mask;
y += a->w[j] * a->d[idx];
sigma += a->d[idx] * a->d[idx];
}
double dsamp;
int base, n1;
dsamp = in_buff[2 * i + 0];
d[in_idx] = dsamp;
base = (in_idx + delay) & mask;
if ((n1 = dline_size - base) > n_taps) n1 = n_taps;
y = 0.0;
sigma = 0.0;
anf_dot (w, d + base, n1, &y, &sigma);
if (n1 < n_taps)
anf_dot (w + n1, d, n_taps - n1, &y, &sigma);
inv_sigp = 1.0 / (sigma + 1e-10);
error = a->d[a->in_idx] - y;
error = dsamp - y;
a->out_buff[2 * i + 0] = error;
a->out_buff[2 * i + 1] = 0.0;
out_buff[2 * i + 0] = error;
out_buff[2 * i + 1] = 0.0;
if((nel = error * (1.0 - a->two_mu * sigma * inv_sigp)) < 0.0) nel = -nel;
if((nev = a->d[a->in_idx] - (1.0 - a->two_mu * a->ngamma) * y - a->two_mu * error * sigma * inv_sigp) < 0.0) nev = -nev;
if((nel = error * (1.0 - two_mu * sigma * inv_sigp)) < 0.0) nel = -nel;
if((nev = dsamp - (1.0 - two_mu * ngamma) * y - two_mu * error * sigma * inv_sigp) < 0.0) nev = -nev;
if (nev < nel)
{
if ((a->lidx += a->lincr) > a->lidx_max) a->lidx = a->lidx_max;
if ((lidx += lincr) > lidx_max) lidx = lidx_max;
}
else
{
if ((a->lidx -= a->ldecr) < a->lidx_min) a->lidx = a->lidx_min;
if ((lidx -= ldecr) < lidx_min) lidx = lidx_min;
}
a->ngamma = a->gamma * (a->lidx * a->lidx) * (a->lidx * a->lidx) * a->den_mult;
ngamma = gamma * (lidx * lidx) * (lidx * lidx) * den_mult;
c0 = 1.0 - a->two_mu * a->ngamma;
c1 = a->two_mu * error * inv_sigp;
c0 = 1.0 - two_mu * ngamma;
c1 = two_mu * error * inv_sigp;
for (j = 0; j < a->n_taps; j++)
{
idx = (a->in_idx + j + a->delay) & a->mask;
a->w[j] = c0 * a->w[j] + c1 * a->d[idx];
}
a->in_idx = (a->in_idx + a->mask) & a->mask;
anf_update (w, d + base, n1, c0, c1);
if (n1 < n_taps)
anf_update (w + n1, d, n_taps - n1, c0, c1);
in_idx = (in_idx + mask) & mask;
}
a->in_idx = in_idx;
a->lidx = lidx;
a->ngamma = ngamma;
}
else if (a->in_buff != a->out_buff)
memcpy (a->out_buff, a->in_buff, a->buff_size * sizeof (complex));
+95 -28
View File
@@ -26,6 +26,45 @@ warren@wpratt.com
#include "comm.h"
/* Filter output and tap-window energy over a unit-stride run of the delay line.
The delay line is indexed (in_idx + j + delay) & mask, which wraps at most
once across the tap window; xanr() splits the window at the wrap so both
halves are contiguous here. Four independent accumulator pairs keep the FMAs
off a single dependency chain and let the vectorizer in -- an 'y += w[j]*x[j]'
reduction cannot be reassociated without -ffast-math, which this library must
not enable (it relies on IEEE semantics for 0/0 = NaN and x/0 = Inf). */
static inline void anr_dot (const double* WDSP_RESTRICT w,
const double* WDSP_RESTRICT x, int n, double* py, double* psigma)
{
double y0 = 0.0, y1 = 0.0, y2 = 0.0, y3 = 0.0;
double s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0;
int j = 0;
for (; j <= n - 4; j += 4)
{
y0 += w[j + 0] * x[j + 0]; s0 += x[j + 0] * x[j + 0];
y1 += w[j + 1] * x[j + 1]; s1 += x[j + 1] * x[j + 1];
y2 += w[j + 2] * x[j + 2]; s2 += x[j + 2] * x[j + 2];
y3 += w[j + 3] * x[j + 3]; s3 += x[j + 3] * x[j + 3];
}
for (; j < n; j++)
{
y0 += w[j] * x[j];
s0 += x[j] * x[j];
}
*py += (y0 + y1) + (y2 + y3);
*psigma += (s0 + s1) + (s2 + s3);
}
/* Leaky-LMS tap update over the same unit-stride run. */
static inline void anr_update (double* WDSP_RESTRICT w,
const double* WDSP_RESTRICT x, int n, double c0, double c1)
{
int j;
for (j = 0; j < n; j++)
w[j] = c0 * w[j] + c1 * x[j];
}
ANR create_anr (
int run,
int position,
@@ -81,53 +120,81 @@ void destroy_anr (ANR a)
void xanr (ANR a, int position)
{
int i, j, idx;
int i;
double c0, c1;
double y, error, sigma, inv_sigp;
double nel, nev;
if (a->run && (a->position == position))
{
for (i = 0; i < a->buff_size; i++)
{
a->d[a->in_idx] = a->in_buff[2 * i + 0];
const int n_taps = a->n_taps;
const int dline_size = a->dline_size;
const int mask = a->mask;
const int delay = a->delay;
const int buff_size = a->buff_size;
const double two_mu = a->two_mu;
const double gamma = a->gamma;
const double den_mult = a->den_mult;
const double lincr = a->lincr;
const double ldecr = a->ldecr;
const double lidx_min = a->lidx_min;
const double lidx_max = a->lidx_max;
/* in_buff and out_buff are the same buffer in RXA, so neither may be
marked restrict; d and w are private to the struct. */
const double* in_buff = a->in_buff;
double* out_buff = a->out_buff;
double* WDSP_RESTRICT d = a->d;
double* WDSP_RESTRICT w = a->w;
int in_idx = a->in_idx;
double lidx = a->lidx;
double ngamma = a->ngamma;
y = 0;
sigma = 0;
for (j = 0; j < a->n_taps; j++)
for (i = 0; i < buff_size; i++)
{
idx = (a->in_idx + j + a->delay) & a->mask;
y += a->w[j] * a->d[idx];
sigma += a->d[idx] * a->d[idx];
}
double dsamp;
int base, n1;
dsamp = in_buff[2 * i + 0];
d[in_idx] = dsamp;
base = (in_idx + delay) & mask;
if ((n1 = dline_size - base) > n_taps) n1 = n_taps;
y = 0.0;
sigma = 0.0;
anr_dot (w, d + base, n1, &y, &sigma);
if (n1 < n_taps)
anr_dot (w + n1, d, n_taps - n1, &y, &sigma);
inv_sigp = 1.0 / (sigma + 1e-10);
error = a->d[a->in_idx] - y;
error = dsamp - y;
a->out_buff[2 * i + 0] = y;
a->out_buff[2 * i + 1] = 0.0;
out_buff[2 * i + 0] = y;
out_buff[2 * i + 1] = 0.0;
if((nel = error * (1.0 - a->two_mu * sigma * inv_sigp)) < 0.0) nel = -nel;
if((nev = a->d[a->in_idx] - (1.0 - a->two_mu * a->ngamma) * y - a->two_mu * error * sigma * inv_sigp) < 0.0) nev = -nev;
if((nel = error * (1.0 - two_mu * sigma * inv_sigp)) < 0.0) nel = -nel;
if((nev = dsamp - (1.0 - two_mu * ngamma) * y - two_mu * error * sigma * inv_sigp) < 0.0) nev = -nev;
if (nev < nel)
{
if ((a->lidx += a->lincr) > a->lidx_max) a->lidx = a->lidx_max;
if ((lidx += lincr) > lidx_max) lidx = lidx_max;
}
else
{
if ((a->lidx -= a->ldecr) < a->lidx_min) a->lidx = a->lidx_min;
if ((lidx -= ldecr) < lidx_min) lidx = lidx_min;
}
a->ngamma = a->gamma * (a->lidx * a->lidx) * (a->lidx * a->lidx) * a->den_mult;
ngamma = gamma * (lidx * lidx) * (lidx * lidx) * den_mult;
c0 = 1.0 - a->two_mu * a->ngamma;
c1 = a->two_mu * error * inv_sigp;
c0 = 1.0 - two_mu * ngamma;
c1 = two_mu * error * inv_sigp;
for (j = 0; j < a->n_taps; j++)
{
idx = (a->in_idx + j + a->delay) & a->mask;
a->w[j] = c0 * a->w[j] + c1 * a->d[idx];
}
a->in_idx = (a->in_idx + a->mask) & a->mask;
anr_update (w, d + base, n1, c0, c1);
if (n1 < n_taps)
anr_update (w + n1, d, n_taps - n1, c0, c1);
in_idx = (in_idx + mask) & mask;
}
a->in_idx = in_idx;
a->lidx = lidx;
a->ngamma = ngamma;
}
else if (a->in_buff != a->out_buff)
memcpy (a->out_buff, a->in_buff, a->buff_size * sizeof (complex));