Repository navigation
Preserve subnormal results in portable exp2 - #11115
Conversation
|
#11116 is merged, fixing both upstream comment-spacing violations. Could a maintainer rerun Buildkite on this PR against the updated master? |
|
Try rebasing on top of master now that it has that commit. |
867ea7a to
7ca5aca
Compare
Looks like a previous commit broke that one. |
Fixed in commit 26d4c26 |
|
I checked the macOS log: debug64 fails the existing assertion The assertion and the zero-input path in Separately, Buildkite #10491 fails during installation of pre-commit because pip cannot uninstall Debian’s |
Do not return zero as soon as the input falls below the exponent of the smallest normal value. Preserve representable subnormal results and values above the half-smallest-subnormal boundary that round up instead of to zero. Evaluate this range after an exact mantissa-width exponent shift, then scale the normal intermediate with one final multiplication. This also avoids truncation of subnormal results by some ldexp implementations. Keep the existing evaluation path for ordinary inputs. Test exact powers throughout the subnormal range for float, double, and real, including the public overloads. Exercise the portable implementation directly at and around the rounding boundary and at fractional exponents. Subnormal evaluations now perform the computation that the original early return skipped; benchmark results and the resulting performance tradeoff are documented in the PR description. Fixes dlang#11114 AI-Assisted-By: Codex (GPT-6 Astra Medium)
Return zero explicitly at half the smallest subnormal so excess intermediate precision does not retain a nonzero halfway result. Store fractional regression results in their declared floating-point type before checking subnormal rounding, since x87 registers can retain excess precision for float and double. AI-Assisted-By: Codex (GPT-6 Astra Medium)
26d4c26 to
9656d13
Compare
|
Rebased to master, prior failing MacOS x64 test no longer failing |
Fixes #11114.
The portable
exp2implementation returns zero below the exponent of the smallest normal value, discarding representable subnormal results. For example,exp2(-1023.0)returns zero instead ofdouble.min_normal / 2.Changes
For inputs in the subnormal-output range, shift the exponent upward by
T.mant_dig, evaluate a normal intermediate using the existing approximation, and multiply by2^(-T.mant_dig)once to round the final result. The exponent shift is exact in this range and requires only one additional evaluation level.Retain an early zero return below the half-smallest-subnormal boundary. At that boundary and above it, perform the final rounding instead of prematurely returning zero. This avoids relying on
ldexpto round subnormal outputs: widening the cutoff alone still failed the nearest-input-above-half test on Windows LDC because itsldexpimplementation truncated the result.The ordinary-input path, approximation coefficients, and existing x87 assembly implementation are unchanged.
Regression tests cover every exactly representable integer power from the smallest normal through the smallest subnormal, public overloads for
float,double, andreal, and direct portable evaluation at and on either side of the half-smallest-subnormal threshold. Fractional-exponent cases check rounding near the bottom of the range. Boundary expectations use round-to-nearest, ties-to-even.Validation
std.math.exponentialmodule tests pass in debug and optimized builds, with-preview=dip1000as required by existing tests.--noasm: the full module tests pass in debug and optimized builds, also with DIP1000 enabled.real. The full module suite cannot compile against this installation's oldercore.int128API; the samepowmod/mulsignature error was reproduced with the unchanged source.realhas double precision. Maximum error is one smallest-subnormal unit. These checks do not establish correct rounding for every input; the existing approximation can still differ by one unit.git diff --checkpasses.Performance versus the original
Baseline: upstream commit
1877367354fadf001365292d74e39ffd15c21bbc.Benchmarks compile the original and fixed portable helper implementations under separate module names and call them through equivalent function pointers. This explicitly measures the portable implementation even where public
exp2(real)uses assembly; the assembly path is not changed by this PR.Each timed sample evaluates 512 inputs over 512 repetitions. Results feed an integer checksum of their bit patterns, avoiding extra floating-point arithmetic on subnormal outputs in the harness. Three process runs alternate baseline/fixed order on a pinned CPU. One warm-up pair is excluded, leaving 24 measured pairs per dataset and type.
The following are median nanoseconds per call, original → fixed. Compiler/environment differences mean these should be compared within each row.
Ordinary-input paired median changes were approximately +0.1% to +0.2% with DMD and -0.3% to +0.1% with LDC. GDC measured +1.1% to +4.3% in aggregate, but varied substantially by run: float ranged from -3.2% to +7.9%, and double from -1.9% to +13.0%. These small absolute differences are noisy; they do not establish exact equality of performance or a stable GDC regression.
The near-minimum-normal dataset also stayed close: within roughly 1.2% on Windows and 2.9% on GDC. For true-underflow inputs, the additional cutoff check has a small absolute cost in some cases: DMD float changed from about 1.70 to 1.95 ns, DMD portable real from 14.91 to 16.08 ns, and LDC double/real by roughly 0.1–0.2 ns. Overflow-input measurements stayed within about 1.2%.
Subnormal performance tradeoff
The substantial subnormal slowdown is localized to results the original implementation incorrectly replaced with zero. The fixed implementation performs range reduction, approximation, scaling, and subnormal rounding.
For float/double, this changes roughly 1–2 ns into 53–66 ns. Portable 80-bit real changes roughly 8–15 ns into 220–224 ns on this machine. These measurements compare correct computation with the original early zero return. They are whole-call timings, without isolated profiling of the rounding instructions.
Builds use GDC
-O2, DMD-O -inline, and LDC-O2 --enable-inlining --noasm, without fast-math. GDC runs under WSL; DMD and LDC run on Windows. IEEE 128-bitrealwas not available for testing.This PR was written entirely by an LLM/AI.
AI-Assisted-By: Codex (GPT-6 Astra Medium)