Skip to content

Exp, Log: Estrin polynomial evaluation in RELAXED mode - #18

Draft
Auburn wants to merge 1 commit into
masterfrom
perf/relaxed-exp-log
Draft

Auburn wants to merge 1 commit into
masterfrom
perf/relaxed-exp-log

Conversation

@Auburn

@Auburn Auburn commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Problem

FS::Pow is Exp( pow * Log( value ) ), and Exp and Log (ToolSet/Generic/Functions.h) evaluate their polynomials with Horner's scheme. So the 9 coefficient Log polynomial and the 6 coefficient Exp polynomial are each one long chain of dependent multiply-adds, and Pow runs one after the other. Code that calls Pow a lot, like FastNoise2's Minkowski distance and PowFloat node, mostly waits on that chain.

Change

In RELAXED mode only, Exp and Log use Estrin's scheme. Pairs of coefficients are combined with independent FMulAdds, and the pairs are then joined with x², x⁴ (and x⁸ for Log). Log also folds its -x²/2 term into the lowest pair. This costs one extra multiply in each function and roughly halves the dependency chain.

Non-relaxed builds keep Horner's scheme, so their results don't change.

This one is optional. RELAXED results round differently in the last bit, and some FastNoise2 cases that never call Pow get slower on GCC (see Costs).

Accuracy

I compiled master's and this branch's functions into one program, with the flags FastSIMD's RELAXED test libraries use. Then I compared both with std::exp/std::log/std::pow in double precision over the test ranges:

  • Exp: every float in [-87.3, 88.37].
  • Log: every float in [1e-20, 1e20].
  • Pow: 2^26 random pairs, with the base in [1e-4, 1e4] and the exponent in [-8, 8].
Max error, master Max error, this PR Mean error Results that change
Exp 1.28 ulp 1.31 ulp (1.28 with FMA) 0.075 ulp, unchanged 0.05-0.11% of inputs, by 1 ulp
Log 0.83 ulp 1.03 ulp 0.25 ulp, unchanged 0.14-0.16%, by 1 ulp
Pow 128 ulp 128 ulp 9.5 ulp, unchanged 0.9-1.6%

These are the same with clang-cl 23, MSVC 19.51 and GCC 16 on SSE2, SSE4.1, AVX2 and AVX512. Homebrew clang 23 on the Apple M1 (NEON, AARCH64) matches the FMA figures. A changed Pow result can move by up to 128 ulp, because Pow scales a 1 ulp change in Log by b * ln( a ).

FastNoise2 builds with fast-math, which reorders these expressions differently per compiler:

  • clang-cl and M1 clang: same accuracy before and after.
  • SSE2/SSE4.1 Log now lands at 1.95-1.96 ulp max and 0.40 mean on every x86 compiler, which is where clang-cl already was. That is worse for MSVC /fp:fast (was 0.83 max, 0.25 mean) and better for GCC 16 -ffast-math (was 2.53 max, 0.46 mean).

Effect

I built FastNoise2 master (8c2d080) against this branch and against FastSIMD master, with the same compiler, and timed both on a Ryzen 9 7950X. Both builds are loaded in one process and timed interleaved on one pinned core. Each figure is the median of 3 processes with alternating order.

The Pow cases are the four Minkowski distance nodes (DistanceToPoint and CellularValue/Distance/Lookup, with a fixed and a node-driven P where available) plus PowFloat, in 2D, 3D and 4D. The controls are the Euclidean CellularValue/Distance/Lookup cases, which compile into the same functions but never call Pow, plus PowInt. I left out the SSE2 4D PowFloat/PowInt cases, which move with code placement. Numbers are new/old time, geomean per feature set:

Compiler SSE2 SSE4.1 AVX2 AVX512 Controls
clang-cl 23 0.788 0.759 0.870 0.957 0.995
MSVC 19.51 (VS 2026) 0.828 0.800 0.781 0.941 1.000
MSVC 19.44 (VS 2022) 0.807 0.794 0.817 0.938 0.987
GCC 16 (Linux) 0.819 0.801 0.930 0.972 1.013

On the Apple M1 with Homebrew clang 23 (unpinned, so noisier), NEON is 0.865 and AARCH64 0.866, with controls at 0.978 and 0.995.

  • Biggest cases: on SSE2/SSE4.1, Minkowski DistanceToPoint is 0.72-0.84 and the cellular Minkowski cases 0.73-0.86 with every compiler. PowFloat is 0.86-0.95 there.
  • On top of Exp, Log, Pow: fewer instructions, bit-identical results #17 (that branch with this one merged, against that branch): clang-cl 0.782/0.763/0.849/0.953, MSVC 19.51 0.813/0.794/0.776/0.918, MSVC 19.44 0.815/0.788/0.781/0.934, GCC 16 0.808/0.798/0.909/0.982, M1 0.834/0.833.
  • Default benchmark set (no Pow), all feature sets: clang-cl 0.997 and MSVC 19.51 0.997.
  • Earlier measurements on a FastNoise2 development branch agree. They were taken on a busier machine, on top of the Exp, Log, Pow: fewer instructions, bit-identical results #17 commits: clang-cl 23 0.82/0.80/0.87/0.96, MSVC 19.51 0.85/0.83/0.81/0.92.

Costs

  • GCC 16, AVX2, Euclidean cellular cases: CellularDistance 2D/3D are 1.17/1.12, and CellularLookup 2D/4D 1.11/1.10 (7 runs, all within 1.5%). On top of Exp, Log, Pow: fewer instructions, bit-identical results #17 they are 1.04-1.10.
    • GCC inlines FastNoise2's distance switch, so the Minkowski Pow shares the generator loop with the Euclidean path, and FastNoise2's AVX2 code works on pairs of 8-wide registers. The cellular generators' stack frames grow by 160-256 bytes, and their stack accesses by 14-22%.
    • Using Estrin's scheme in Exp only gains at most 4% but slows these cases just as much. So any bigger Pow does this here, and the fix belongs in FastNoise2, by keeping the Minkowski path out of that loop.
  • MSVC 19.51 and M1, Euclidean cellular cases: these move both ways with register allocation. MSVC keeps FastNoise2's CalcDistance out of line, with all the distance functions in one switch.

Testing

  • FastSIMD tests, full count (4,194,304 inputs), seeds 24301, 1 and 2:
    • clang-cl 23, MSVC 19.51, GCC 16, Emscripten 6.0.12 (node), M1 Homebrew clang 23 and M1 Apple clang 21 each give the same list as master, with 0 failures.
    • The covering tests are f32 exp, f32 log, f32 pow and f32 pow zero base, whose RELAXED tiers must stay within 8/8/256 ulp of the non-relaxed SCALAR result. With --stats the largest RELAXED SIMD differences are now 1, 1 and 127 ulp. On master they are 0, because RELAXED ran the same Horner code. The non-relaxed f32 exp/log/pow full range tests still match SCALAR bit for bit.
  • FastNoise2 output against FastSIMD master: no case differs by more than 1e-3 of its output range, with any compiler.
    • With clang-cl 23, MSVC 19.44, GCC 16 and M1 clang only the Pow cases change (48 on x86, 24 on M1). Minkowski DistanceToPoint changes by up to 2.6e-6 and the rest by up to 3e-7.
    • MSVC 19.51 changes 51 Pow cases (up to 4.3e-6) and also 37 Euclidean cellular cases, 34 of them on SSE2. That is the out-of-line CalcDistance again. Most of those are below 3e-6, and four are 1e-4 to 4.2e-4 (F3F4Div is the largest).
  • FastNoise2 also builds for WASM (Emscripten 6.0.12) and for ARM NEON/AARCH64 (clang 23 cross compile, and on the M1).

Not included

The work this came from also replaced the two-part (Cody-Waite) ln 2 constant with a single 0.693147180559945309f in RELAXED mode. MSVC keeps the two parts even with /fp:fast, so that gave MSVC another 3% on the Pow cases. But without fast-math it breaks f32 exp RELAXED: 27 ulp in the test, and up to 68 ulp over the range. For |x| above 64 the single product fx * ln2 rounds by up to 3.8e-6, and the range reduction turns that into relative error. So I left it out.

clang-cl's /fp:fast builds of master already merge the two parts, and their Exp has that 68 ulp error on SSE2/SSE4.1 (4.6 ulp with FMA). That may be worth a separate look.

Other PRs

…ing)

Pow (Exp of a scaled Log) is latency bound: Horner's scheme makes the 9 term
log polynomial and the 6 term exp polynomial long dependency chains. In
RELAXED mode evaluate them with Estrin's scheme instead (two more multiplies,
about half the latency). Results round differently in the last bit: over the
test input ranges the mean ulp error is unchanged, and the max error is
1.31 ulp for Exp (was 1.28) and 1.03 ulp for Log (was 0.83).
Non-relaxed builds keep Horner's scheme and their exact results.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant