Repository navigation
Conversation
Keeps editor/agent saves byte-exact; repo style (2-space indent, no tabs, per AGENTS.md) is applied only via deliberate formatting runs.
The setup structure is read-only during transforms (twiddle/ifac tables are only written by the *_ti1_ps init functions), so all functions that merely consume it now take const PFFFT_Setup * / PFFFTD_Setup *. Source-compatible with existing callers; allows passing shared setups from threads without casting away const.
MSVC users may define _USE_MATH_DEFINES as a compiler-level option; unconditionally redefining it in the source is at best redundant and at worst a macro-redefinition warning. Only define it if not already set.
Name API functions by what they do, not what they omit: zconvolve_accumulate : ab += a*b*scaling zconvolve_scale : ab = a*b*scaling (was no_accu) The old name stays as a deprecated synonym forwarding to the new one, so existing callers keep linking and running unchanged.
dft_ab = dft_a * dft_b -- no accumulation, no scaling argument. The compile-time-absent scaling factor saves one vector multiply per SIMD group compared to pffft_zconvolve_scale(.., 1.0). Also migrate the benchmark's circular-convolution check (previously memset + zconvolve_accumulate(scaling=1.0), an exact equivalent) and cover float and double in the ctest suite.
pffft_zconvert_zp() turns an unordered forward-transform result into 'zero-phase' form: imaginary vector lanes are zeroed, real lanes are scaled; the DC/Nyquist lane pair is preserved component-wise. A linear-phase FIR centered at t = 0 with left-hand taps wrapped to the end of the block yields such a purely-real spectrum. pffft_zconvolve_zp() then multiplies a block spectrum with that filter spectrum using two vector multiplies per real/imag vector pair where a full complex multiply needs four: Y_re = X_re*H_re, Y_im = X_im*H_re. Micro-benchmark vs pffft_zconvolve_scale(.., 1.0) on M2/NEON, best of 5: N=128: 1.53x, N=512: 1.60x, N=2048: 1.59x, N=8192: 1.47x faster. Both return nonzero for non-REAL setups. Float and double, SIMD and scalar paths. C++ wrapper: zconvertZP()/convolveZP().
Round-trip check: linear-phase FIR wrapped around the block, converted with pffft_zconvert_zp() and applied with pffft_zconvolve_zp(), must reproduce the time-domain circular convolution (float and double). Cross-checks the zp kernel against the plain complex multiply outside the DC/Nyquist lanes and verifies both functions reject PFFFT_COMPLEX setups. Verified in SIMD and -DPFFFT_USE_SIMD=OFF builds.
The scalar-fallback kernel did ab[0] += .. / ab[N-1] += .. where the no-accumulate contract (and the SIMD sibling) require plain stores, leaving stale output values in bins 0 and N/2. Pre-existing defect surfaced by review; note the nosimd branch is currently unreachable through CMake -- scalar builds use the 4xScalar emulation with SIMD_SZ == 4 -- so no test configuration exercises it directly.
pffft_min_fft_size(PFFFT_COMPLEX) returns SIMD_SZ^2, which is 1 in the nosimd branch reachable via PFFFT_USE_SCALAR_VECT=OFF. The size sweep then started at N = N_min/2 == 0 and stepped by 0 -- an endless stream of rejected new_setup(0) calls that looked like a library hang. Step by 1 when N_min < 2.
This is the only configuration that compiles the SIMD_SZ==1 nosimd branch -- previously never executed by any test configuration. Excludes the greenffts octave wrappers that cannot run outside their environment; everything else must pass.
_USE_MATH_DEFINES guardAddress issue #101 feedback: with the unnormalized forward transform, the zp pipeline carried a factor N, forcing callers to pass scaling = 1/N to zconvert_zp to get correct convolution gain. zconvert_zp() now applies scaling/N internally (both SIMD and nosimd variants, including the Nyquist lane), so with scaling == 1 the pipeline transform(x, FORWARD) -> zconvolve_zp() -> transform(.., BACKWARD) yields the circular convolution at unit gain, matching r8brain's convention. 'scaling' becomes an additional gain. The normalization lives at filter-conversion time, so the per-block zconvolve_zp kernel is unchanged. Tests: round-trip now asserts Y ~= direct circular convolution without rescaling; the bit-exact cross-check against zconvolve_scale converts a second filter copy with scaling == N, which cancels the internal 1/N exactly. Wrapper test updated likewise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Convolution improvements — const setup, unscaled zconvolve, zero-phase convolution,
_USE_MATH_DEFINESguardImplements all four suggestions from #101 for both the float and double variants (which share one implementation header, so every change lands in both automatically). One suggestion is implemented with a design refinement specific to PFFFT's memory layout — see Zero-phase convolution below.
1.
constsetup pointersAll functions that only consume the setup (
pffft_transform,pffft_transform_ordered,pffft_zreorder,pffft_zconvolve_*) now takeconst PFFFT_Setup */const PFFFTD_Setup *. Verified read-only: the twiddle/ifac tables are written exclusively by the*_ti1_ps()init functions at setup creation;rfftf1_ps/cfftf1_psalready took(const float *wa, const int *ifac). Source-compatible with every existing caller; makes sharing one setup across threads formally correct.2. Unscaled
pffft_zconvolve()New entry point computing
dft_ab = dft_a * dft_bwith no accumulation and no scaling argument. The scaling factor was a runtime value in the previous kernels, so the compiler could not fold away the dead multiply; removing it saves one vector multiply per SIMD group. As suggested in the issue, rescaling belongs on the static filter side.Naming: the fork-specific
pffft_zconvolve_no_accu()(which upstream never had) is renamed topffft_zconvolve_scale()so the family reads by what each function does:The old name remains as an exported deprecated synonym forwarding to the new symbol — existing binaries keep linking.
3. Zero-phase convolution —
pffft_zconvert_zp()+pffft_zconvolve_zp()Implements the r8brain trick for linear-phase FIRs centered at t=0 with left-hand taps wrapped around the block. One necessary refinement to the proposal: in PFFFT's unordered output the coefficients are not stored as interleaved re/im pairs — whole SIMD vectors alternate between real parts and imaginary parts, and F(0)/F(N/2) pack into the first lanes of vectors 0/1 (with the real-valued Nyquist coefficient sitting in an otherwise-imaginary lane). A flat sequential multiply therefore cannot work on unordered buffers directly. Instead this PR provides:
pffft_zconvert_zp(setup, in, out, scaling)— converts an unordered REAL forward spectrum once at filter-setup time: imaginary vector lanes are zeroed, real lanes scaled, the DC/Nyquist pair preserved component-wise.pffft_zconvolve_zp(setup, x, hzp, ab)— multiplies any number of block spectra against the converted filter using two vector multiplies per re/im vector pair where a full complex multiply needs four (Y_re = X_re·H_re,Y_im = X_im·H_re).This keeps the faster unordered transforms end-to-end, which is the point of the optimization. Measured vs
zconvolve_scale(.., 1.0)on M2/NEON, best of 5: 1.53× / 1.60× / 1.59× / 1.47× at N = 128/512/2048/8192. Both functions return nonzero for non-REAL setups; aliasing (in == out) is supported and documented, including the DC/Nyquist lane caveat in the public headers.C++ wrapper:
convolve(a, b, ab),zconvertZP()andconvolveZP()added to the publicFft<T>template (raw-pointer andAlignedVectorforms); COMPLEX instantiations forward unchanged and are rejected by the underlying functions.4.
_USE_MATH_DEFINESNow guarded with
#ifndefso defining it as a compiler-level option no longer conflicts.Also included
pffft_zconvolve_scale_nosimd()accumulated instead of assigned at the two REAL boundary lanes (bins 0 and N/2), andtest_fft_factorslooped forever underSIMD_SZ == 1becausemin_fft_size(COMPLEX)returns 1 there (N += N_min/2stepped by zero).build_no-simd_novectconfiguration (PFFFT_USE_SIMD=OFF -DPFFFT_USE_SCALAR_VECT=OFF) that compiles and ctests the SIMD_SZ==1 branch — previously never executed anywhere.Testing
zconvolveandzconvolve_scale(.., 1.0); zero-phase round-trip against independently computed time-domain circular convolution; sentinel-based invariant that conversion writes every destination lane; scaling linearity; in-place aliasing equivalence; REAL-only guards; wrapper-level equivalents of all of the above. Red→green verified for the lane-zeroing fix.PFFFT_USE_SIMD=OFF(4×scalar emulation),PFFFT_USE_SIMD=OFF + PFFFT_USE_SCALAR_VECT=OFF(SZ=1 nosimd), and float-less double-only builds.