Fast-trigonometry evaluation¶
This note records why Pulp ships bounded-cycle minimax sine profiles, where
they help, and where they do not. It is evidence for maintainers, not a promise
that every sin call should change. Reference math remains the default.
Why fast trigonometry can matter¶
An oscillator may evaluate sine once per output sample; additive and FM voices
multiply that cost by every active partial or operator. At 48 kHz, a 64-partial
bank can therefore request more than three million sine values per second for
one voice. A bounded normalized phase also gives an implementation more useful
information than a general sin(x): the input is already finite, periodic, and
in a small known interval, so range reduction and a general-purpose library
call may be avoidable.
That arithmetic does not prove a product win. A real voice also advances phase, ramps parameters, reads modulation, updates envelopes and filters, mixes lanes, and may use double precision for continuity. The useful question is therefore not "which sine expression is fastest?" but "which qualified implementation makes this named consumer materially faster without changing its sound or state contract?" Pulp first checks numerical error, then primitive shape, and finally the complete consumer. Reference math remains the compatibility baseline and the default when that chain of evidence is absent.
Selected paths¶
Pulp adapts the degree-5 and degree-9 normalized-cycle sine expressions from Lasse Schlör's Fast MiniMax Polynomial Approximations of Sine and Cosine, pinned and attributed as described in Licensing and acknowledgements.
FmOperatorEngineT<float>may opt into degree 5 (realtime_efficient) or degree 9 (realtime_precise). The two-operator efficient consumer measured a 13.26% whole-engine gain; reference remains the compatibility default.AdditiveBankT<float>may opt intorealtime_precise. On Apple arm64/Clang it evaluates four partials together while retaining double phase accumulators, wrap-event behavior, gain/frequency ramps, and envelope state. Other targets preserve the scalar profile semantics but do not claim acceleration.- FM8 research evaluated a degree-13 candidate because its harmonic reader can execute 32 double sine calls per sample. It passed performance but failed the predeclared broadened hit-quality gate, so no FM8 selector ships.
- Double FM6, all drum engines, scalar additive evaluation, setup-time trig, and test oracles remain on reference math because their consumer evidence did not justify or authorize a replacement.
Adoption census¶
| Pulp surface | Current disposition | Why |
|---|---|---|
| Float FM operators | Opt-in degree 5 or 9; reference default | The efficient two-operator voice cleared the whole-consumer gate |
| Float additive banks | Opt-in degree 9; reference default | Apple arm64 four-wide evaluation cleared performance and audio gates; other targets preserve semantics without a speed claim |
| Double FM6 drum voices | deferred_no_win |
The actual voice improved, but missed the 10% whole-consumer gate |
| FM8 drums | Reference; deferred_quality_no_go |
Degree 13 cleared performance, but a broadened 0.75-second hit corpus found a −38.63% peak change, outside the predeclared ±25% bound |
| Other kick, snare, clap, cymbal, and percussion voices | Reference; opportunity not yet measured | FM6 did not clear its gate and FM8's distinct density does not authorize a call-site cascade; profile each complete voice before choosing sine or another math target |
| SSB/frequency shifting | deferred_more_evidence |
A paired degree-13 experiment screened well, but lacks a second primary target and a platform paired/vector baseline |
| Non-Apple CPU and WebAssembly additive paths | Reference or scalar profile semantics | No whole-consumer acceleration has been established on those targets |
| GPU kernels | Native shader trig | CPU evidence does not establish a shader/device win; two named backend/device families are required |
| Setup, filters, FFT preparation, and test oracles | Reference | They are not demonstrated realtime bottlenecks, and oracles must remain independent |
| SDK/component authoring | Specific opt-in profiles | Public C++ consumer APIs own selection, and capability metadata advertises accepted trig support or effective state where implemented; there is no global fast-trig switch |
| Generated DSP | No separate fast-trig generator control | Generated consumers can use accepted component APIs, and the vocabulary exposes the FastMath primitive signature; it adds no top-level component selector or forked coefficient tables |
| Live Unified Control broker control | deferred_missing_exact_live_consumer |
Add a granted live selector only when a named product binds an accepted component and can report its effective profile |
| Swift/Surge | covered, not adopted for realtime |
Surge at inspected commit ac638794 is an MIT Swift convenience layer over Accelerate/vForce; direct vForce already lost the float-additive realtime screen |
The existing audio-harness skill
owns the repeatable DSP measurement workflow. A fast-trig-specific skill would
duplicate it, so the executable benchmarks and this decision record are the
maintenance surface. Production degree-5/9 attribution lives in NOTICE.md;
its source and permission details live in
Licensing and acknowledgements.
Why the remaining drum voices stayed on reference math¶
This is a stop-rule result, not a finding that alternative math cannot help drums. The double FM6 actual-voice pilot improved by only 6.8–7.1% at the median, below the predeclared 10% whole-consumer gate. That made a blind cascade into lower-density or differently structured voices unjustified. FM8 received a separate experiment because its harmonic reader is unusually dense; it cleared the speed gate but failed the broadened hit-quality gate. Neither result proves anything universal about kick, snare, clap, cymbal, hat, tom, membrane, string, or zap voices.
The current implementations contain several distinct possible hot paths:
| Voice family | Candidate work visible in the realtime path | What to establish before changing it |
|---|---|---|
| Kick and tom | Carrier/modulator sine, pitch-sweep exp2, and optional nonlinear output stages |
Profile representative hits and isolate the share of time in each math family |
| Snare | Two body sines, rattle modulation, and pitch/noise exp2 sweeps |
Measure the complete transient and tail; preserve body/noise balance and peak behavior |
| Clap, hat, and cymbal | Body, grit, or strike sine plus noise/filter/resonator work | Prove trig is material rather than assuming one visible call dominates the voice |
| Membrane and string | Sub/modulation sine, pitch response, and modal/string state updates | Separate per-sample work from trigger/setup coefficient construction |
| Zap and shared output stages | Pitch exp2, a ring cosine, and optional sine/tanh shaping |
Compare alternative math only in enabled product configurations |
A follow-up should start with a Release whole-voice profile across named presets,
sample rates, and block sizes. Advance only a math family whose measured share
can plausibly yield more than a 10% voice win. Then test the smallest relevant
candidate—bounded sine, exp2, nonlinear shaping, or coefficient preparation—
against hit peaks, envelopes, spectra, tails, determinism, state continuity,
and allocation behavior. This avoids spending a cycle optimizing a sine call in
a voice dominated by filters, noise, or nonlinear processing, while keeping the
drum opportunity explicitly open.
How the additive decision was tested¶
The benchmark is advisory and Release-only; timing never gates heterogeneous
CI. The recorded run used an Apple M3 Ultra, macOS 26.6.2, Apple Clang 21.0.0,
arm64, and verified both the CMake cache and target flags contained
-O3 -DNDEBUG.
Screening first rendered 64 float partials for 4,096 frames, 15 trials and five
passes per trial. It compared scalar sinf, scalar degree 9, Apple
simd::sinpi, four-wide degree 9, Accelerate/vForce vvsinpif, and oscillator
recurrence. The four-wide polynomial was 41.5–45.5% faster than scalar sinf
across three independent processes. simd::sinpi gained only 9–14%, vForce was
57–69% slower, and recurrence accumulated 1.82e-4 maximum error within 4,096
frames despite a 29–38% speed gain.
The winning candidate was then measured inside the real AdditiveBankT<float>
consumer after profile dispatch was hoisted out of the partial loop so the
reference lane retained its original hot expression. A 64-partial organ at
48 kHz produced 47.8%, 47.8%, and 48.9% whole-bank gains across three
independent processes. The pilot's maximum rendered difference was one float
ULP.
Finally, three independent 54-cell matrices covered:
- organ and bell voices (the bell uses detuned doublets);
- 44.1, 48, and 96 kHz;
- 16, 64, and 128 partials; and
- 32, 128, and 512-sample host blocks.
Each cell used 4,096 frames, nine trials, three passes, weighted full-buffer checksums emitted for both lanes, and alternating reference/candidate order. Every cell won. Minimum gains were 37.8–40.4%, median gains 49.2–49.9%, and p95 gains 64.4–65.3% across three corrected processes. The final rebased full-checksum rerun remained consistent: 39.80% minimum, 47.59% median, and 63.83% p95 across the 54 cell-median gains. The benchmark also emits each lane's p95 render cost rather than conflating it with that across-cell summary. Focused acceptance additionally covers odd remainders, organ/bell outputs, doublets, the public 128-partial maximum (256 rendered bell-doublet slots), allocation freedom, a maximum two-float-LSB rendered-output deviation, and the existing −100 dB THD requirement.
The deployed Forge-style next() shape was also measured directly against
unmodified Pulp dcd739c18db. Seven independent, alternating-order Release
pairs rendered a 64-partial organ for 32,768 frames, 21 trials and five passes.
The patched reference-default median was 434.80 ns/frame versus 447.90 ns/frame
on main; every pair was neutral or faster (1.38–8.10%, median 2.89%). The
profile dispatch therefore introduces no measured reference regression in the
per-sample caller that the block benchmark does not model.
Build and run locally with:
cmake -S . -B build-fast-trig -DCMAKE_BUILD_TYPE=Release \
-DPULP_BENCHMARK=ON -DPULP_ENABLE_GPU=OFF -DPULP_BUILD_EXAMPLES=OFF
cmake --build build-fast-trig \
--target pulp-fast-trig-benchmark pulp-fast-trig-apple-bank-benchmark
./build-fast-trig/test/pulp-fast-trig-apple-bank-benchmark
The rejected FM8 implementation, benchmark method, exact-base hash probe,
broadened quality corpus, and rerun commands are preserved on immutable
research snapshot
a860739e50c.
They are deliberately absent from main.
Periodic reassessment¶
Rerun after a material compiler, standard-library, SDK, CPU, or shader change,
or when a new candidate library offers a plausible consumer advantage. Record
the date, Pulp source SHA, dirty state, CPU/device, OS, compiler, Release flags,
benchmark schema, three independent summaries, quality result, and resulting
adopt, covered, deferred_no_win, deferred_more_evidence,
deferred_quality_no_go, or not_applicable verdict. Append a new dated
finding; do not rewrite the historical row to make an old decision look current.
Keep primitive-only results labeled separately from whole-consumer evidence so
a faster approximation is not mistaken for a demonstrated product speedup.
Nathan Blair's 2026
root-factored sine candidate
was evaluated as a bounded local-only reassessment on 2026-08-27 and is not
adopted Pulp code. Its published Apple M4 chart measures scalar float with
signed phase already reduced and Clang -O3 -ffast-math. The degree-11 Horner
form reports 2.80e-7 maximum float error and about -140.2 dB THD; those are
different metrics. Pulp reproduced the peak-error claim under normal production
flags, but it is outside the existing realtime_precise 2.5e-7 contract. Its
x * (x^2 - 0.25) factor does make the cycle-boundary roots exact.
The Pulp run used an Apple M3 Ultra, macOS 26.6.2, Apple Clang 21.0.0,
-O3 -DNDEBUG, arm64, macOS minimum 13.4, and no -ffast-math. A 2^24
uniform cycle sweep plus adjacent-float searches measured 2.802574095e-7
maximum absolute error for Horner and 3.104929449e-7 for the same coefficients
scheduled with Estrin. Shipped degree 9 measured 1.702105797e-7. Two
qualification executions were byte-identical. Both external forms therefore
stopped at deferred_quality_no_go; their incidental first-screen timing was
not interpreted, and no additive, FM, drum, licensing, production, or Unified
Control work followed.
The unredistributed candidate header was transcribed from the linked thread on
2026-08-27 and had SHA-256
a2f9ec78f1d30d9ddec42945dee6352a149d1ffb4c5505704d4922a56c6c9bb0.
The digest lets a future rerun verify the same input without placing the
third-party coefficient text in Pulp.
Reopen only for materially changed coefficients or evaluation, or after a
separate decision changes the semantic quality contract before measurement. A
future candidate still has to pass numerical qualification before Horner's
independent-lane or Estrin's dependency-latency hypotheses reach an actual
consumer. A winner may replace an implementation behind an existing semantic
profile; it does not add an evaluation-order control, weaken the error budget,
or authorize compiler-wide fast-math. The published degree-7 form (4.02e-4
maximum error, about -71.3 dB THD) has no clean general-audio role.
An unredistributed candidate can join the general primitive/FM/64-partial screen
without entering Pulp source. Supply a local header that defines
pulp_fast_trig_local::report_candidates(report) and calls report(name,
callable) for each bounded-cycle float sine, then configure with:
cmake -S . -B build-fast-trig-candidate -DCMAKE_BUILD_TYPE=Release \
-DPULP_BENCHMARK=ON -DPULP_ENABLE_GPU=OFF -DPULP_BUILD_EXAMPLES=OFF \
-DPULP_FAST_TRIG_LOCAL_CANDIDATES_HEADER=/absolute/path/candidates.hpp
tools/ci/governed-build.sh cmake --build build-fast-trig-candidate \
--target pulp-fast-trig-benchmark --parallel 4
./build-fast-trig-candidate/test/pulp-fast-trig-benchmark \
--qualification-only --inputs 16777216
Qualification mode emits numerical JSON without running the timing loops. Exit
code zero means measurement completed; it does not mean a candidate passed.
Compare each candidate's fields with the selected profile's contract. Only a
candidate that meets that contract proceeds to the ordinary timing command (the
same binary with no arguments). A screening win then authorizes only an actual-
consumer experiment; it does not change defaults or supersede the audio,
state-continuity, platform, licensing, and whole-consumer gates below. The
dense_rms_error field covers the uniform grid; maxima also include the
adjacent-float searches.
The paired SSB experiment is preserved on the non-production
64150c552359 research snapshot.
Its README contains exact benchmark/test rerun commands, raw summaries,
quantile conventions, quality coverage, and preserved historical selector-probe
outputs. The compile-time probe source is retained, but the rejected runtime
patch and an exact probe runner are not; do not describe those historical probe
results as fully rerunnable. Keep the snapshot as an overlay; its degree-13
pair and public aliases are deliberately absent from main.
Dated findings¶
| Date | Consumer and candidate | Evidence | Decision |
|---|---|---|---|
| 2026-08-26 | Two-operator float FM, degree 5 | 13.26% whole-engine gain | Adopt as opt-in realtime_efficient |
| 2026-08-26 | Double FM6/drum voice, degree 13 | 6.79–7.09% median and 10.62–12.14% p95 across 36 cell medians | deferred_no_win; do not cascade into lower-density drums |
| 2026-08-26 | Apple arm64 float additive bank, four-wide degree 9 | 39.80% minimum, 47.59% median, and 63.83% p95 across 54 final cell medians | Adopt as opt-in realtime_precise |
| 2026-08-26 | Double FM8 drum voice, degree 13 | Immutable research snapshot: 24.16–25.07% minimum, 33.00–33.30% median, and 38.69–40.83% p95 across 36 cell medians; exact-base sample hash matched in seven pairs; broadened hit corpus found peak ratio 0.613740 for algorithm 9/wave base 12/48 kHz |
deferred_quality_no_go; preserve research, ship no selector |
| 2026-08-26 | Double SSB pair, degree 13 | 9.91% minimum, 12.38% median, and 19.02% p95 across 36 cell medians; maximum error 1.05e-13; image, carrier, DC, determinism, and 10-second stability screens passed |
deferred_more_evidence; preserve research harness, not production code |
| 2026-08-27 | External root-factored degree-11 Horner/Estrin | M3 Ultra, Apple Clang 21.0.0, normal Release flags, 2^24 sweep plus adjacent-float search: Horner 2.802574095e-7, Estrin 3.104929449e-7, shipped degree 9 1.702105797e-7; repeated qualification output was byte-identical |
deferred_quality_no_go; both exceed the 2.5e-7 precise budget, so do not run consumer or production work |
The rows are Pulp measurements of the named consumers or local overlays in the named environments, not universal claims about a primitive. An external- candidate row identifies unadopted candidate provenance; it does not make the measurement external. FM8 was evaluated separately because its per-sample harmonic reader has materially different trig density from FM6. Its failed quality result does not authorize another drum: each candidate still requires an actual-voice matrix, representative hit corpus, and at least a 10% gain while preserving the existing audio and state-continuity gates.
The exact two-operator FM and double-FM6 whole-consumer executables were not
retained on main or an immutable public research snapshot, so those two
historical figures are not exactly rerunnable. The general benchmark above can
rescreen primitive and FM-like shapes, but a renewed decision must construct and
preserve a current actual-consumer experiment under the audio-harness contract.
Alternatives and reopen rules¶
The alternatives below are not all the same kind of thing. simd::sinpi and
vForce are Apple execution APIs; Chebyshev and minimax describe ways to choose a
polynomial; Horner and Estrin describe how to schedule a polynomial's arithmetic;
oscillator recurrence changes the algorithm and carries accumulated state; and
RLIBM-ALL targets correctly rounded library results. Compare alternatives only
when they solve the same consumer problem, precision contract, phase domain, and
batching shape.
| Alternative | Result | Reconsider only when |
|---|---|---|
Apple simd::sinpi |
Apple's small fixed-width SIMD sin(pi*x) API was correct but produced a much smaller gain in the float-additive screen than degree 9 |
A compiler/platform change reverses that actual-consumer result; the SSB paired/vector baseline remains unmeasured |
Accelerate vForce batch |
Apple's arbitrary-length array transcendental API was slower for a per-sample 64-partial batch | Batching spans a materially different consumer layout |
Swift Surge at ac638794 |
Convenience façade over Accelerate/vForce, with array allocation and unit-stride batch APIs; no new approximation or C++ realtime advantage | A named Swift-owned offline/batch consumer needs its broader ergonomic API; benchmark direct Accelerate first |
| Oscillator recurrence | Fast, but short-run drift missed the quality objective | A bounded renormalization scheme clears long-hold and consumer gates |
| Scalar degree 9 additive | Slower than reference in prior 64-partial tests | New scalar target/compiler evidence exists |
| Degree-13 double FM6 | Median whole-voice gain only 6.8–7.1%, below the 10% gate | A materially new vectorized double design is proposed |
| Degree-13 double FM8 | Strong speed win, but recursive hit peak changed −38.63% in the broadened corpus | A materially different implementation or explicitly different timbral product contract declares a new quality gate before measurement |
| 2017 Chebyshev approximation | A bounded f32 polynomial reported within 4.58 ULP over its stated interval; it has no current Pulp role after the selected degree-9 path won on Pulp's speed and quality objectives |
A new objective or target demonstrates a whole-consumer advantage over the selected path |
| 2026 root-factored degree-11 Horner/Estrin | Pulp reproduced the published Horner peak-error scale, but Horner and Estrin both failed the existing precise budget under normal Release flags | Materially changed coefficients/evaluation first clear the same error gate; only then may a named consumer seek the 10% whole-consumer gate |
| RLIBM-ALL | Its correctly rounded, multi-representation and multi-rounding-mode goal is broader than Pulp's realtime bounded-phase need | Correct rounding becomes a stated requirement, or an applicable design enters an actual-consumer screen |
| Integer SIMD or assembly | Not required to establish the portable/Apple SIMD win | A portable integer prototype first clears conversion and accuracy gates, then target assembly demonstrates enough additional consumer value to justify its maintenance cost |
| Compiler-wide fast-math | Rejected; changes unrelated floating-point contracts | Never as part of this facility; benchmark only as separately labeled research |
The reopen condition is consumer evidence, not a faster primitive in isolation. Do not replace cached/setup trig, reference oracles, quadrature, GPU native trig, or other components without their own performance and audio-quality gates.