Strict mode - integer-only, correctly-rounded transcendentals¶
Transcendental functions (logarithms, exponentials, roots, trigonometry) come in two forms:
- the f64 bridge - convert to
f64, apply the platformf64intrinsic, convert back. Fast, depends onstdand the platform libm. - the strict form - an integer-only implementation. Platform-
independent, bit-identical on every target,
no_std-compatible.
The *_strict / *_fast dual API¶
Every transcendental that has a strict implementation exposes it
under a *_strict name; every transcendental that has an f64-bridge
implementation exposes it under a *_fast name. Both surfaces are
always compiled regardless of which Cargo feature is active (subject
only to dependency gates - *_fast needs feature = "std"). The
strict / fast Cargo features only choose what the plain * form
resolves to.
use decimal_scaled::D38s12;
let x = D38s12::from_int(2);
// Always available - the integer-only path, explicitly:
let r1 = x.sqrt_strict();
let l1 = x.ln_strict();
// Also always available - the f64-bridge path, explicitly (needs
// `feature = "std"`):
let r2 = x.sqrt_fast();
let l2 = x.ln_fast();
// The plain method dispatches by feature (0.3.0 rule):
// * with `strict` (default) -> calls `*_strict`
// * with neither feature set -> calls `*_strict`
// * with `fast` AND NOT `strict` -> calls `*_fast` (needs std)
// * with both `strict` AND `fast` -> calls `*_strict` (strict wins)
let r3 = x.sqrt();
Why a dual API:
- Run both side by side - benchmark or cross-check the strict path against the f64 bridge in the same build.
- Mix and match - call
ln_strict()for the values that must be deterministic andln_fast()(or plainln()) for the rest. - Guaranteed strict regardless of feature toggles -
*_strictmeans strict, full stop; it cannot be silently swapped for the f64 bridge by a downstream crate flipping a feature. The same applies in reverse:*_fastalways reaches the f64 bridge.
The *_strict surface covers, on D38 (and on D9 / D18 by
widen-compute-narrow delegation):
| Group | *_strict methods |
|---|---|
| Logarithms | ln_strict, log_strict, log2_strict, log10_strict |
| Exponentials | exp_strict, exp2_strict |
| Roots / powers | sqrt_strict, cbrt_strict, powf_strict, hypot_strict |
| Forward trig | sin_strict, cos_strict, tan_strict |
| Inverse trig | asin_strict, acos_strict, atan_strict, atan2_strict |
| Hyperbolic | sinh_strict, cosh_strict, tanh_strict |
| Inverse hyperbolic | asinh_strict, acosh_strict, atanh_strict |
| Angle conversion | to_degrees_strict, to_radians_strict |
The strict feature¶
With strict enabled, the plain methods (sqrt, ln, sin, …)
dispatch to their *_strict form. strict does not require std; the
integer algorithms compile under no_std + alloc. The explicit
float-conversion methods (to_f64, from_f64,
TryFrom<f64>, …) remain available - they are type conversions, not
transcendental operations.
The fast feature¶
fast makes the plain methods (sqrt, ln, sin, …) dispatch
through the f64 bridge for speed at the cost of ~16-digit
platform-libm precision. It only affects what plain * resolves
to: both the *_strict integer-only methods and the *_fast
f64-bridge methods are always emitted so per-call selection
stays available.
Strict-by-default — and strict wins when both are enabled.
0.3.0 changes the dispatcher rule: strict is the default plain
dispatch regardless of whether the strict feature is
explicitly enabled, and fast only takes over plain dispatch
when strict is explicitly absent. Reasoning: the strict path
is now fast enough (ln_strict at D38<19> is ~1.5 µs, sin at
39 µs) that staying on the deterministic correctly-rounded path
by default is the right call across more codepaths. The only
way to land on *_fast for plain sqrt / ln / etc. is to
build with default-features = false AND explicitly enable
fast AND NOT re-enable strict — a deliberate three-step
opt-out. Mixing strict with fast (e.g. a downstream crate
flips fast on a transitive build that also has strict)
keeps you on the strict path.
| Features | *_strict named methods |
*_fast named methods (needs std) |
plain sqrt / ln / … |
|---|---|---|---|
| (none) | present | present | dispatches to *_strict |
strict (default) |
present | present | dispatches to *_strict |
fast (and not strict) |
present | present | dispatches to *_fast (needs std) |
strict + fast |
present | present | dispatches to *_strict |
Strict is the default dispatcher — explicit, intentional. To
get the f64 bridge as the plain dispatch you have to (a) build
with default-features = false (which drops the strict feature
along with std+serde) and (b) add the fast feature plus
std. Mixing strict with fast keeps you on the strict path —
the only way to land on *_fast for sqrt / ln / etc. is to
explicitly opt out of strict.
The 0.5 ULP accuracy guarantee¶
Every strict method is held to the IEEE 754 correctly-rounded standard: the returned value is within 0.5 ULP (unit in the last place) of the mathematically exact result - i.e. it is the exact result rounded to the nearest representable value at the type's last decimal place.
How it is achieved, per function family:
- Algebraic roots (
sqrt,cbrt) form the exact wide-integer radicand (r · 10^SCALEfor sqrt as a 256-bit value,r · 10^(2·SCALE)for cbrt as a 384-bit value), take its exact integer root, and decide the rounding with an exact integer comparison (8·N ≥ (2q+1)³for cbrt, etc.). No approximation enters. - Transcendentals (
ln,log,log2,log10,exp,exp2,powf, and the whole trig / hyperbolic / angle-conversion family) evaluate their range reduction and series in the in-treewide_int::Fixedintermediate - a 256-bit value atSCALE + 30decimal guard digits. The 30 guard digits bound the total accumulated rounding error to roughly1e-17of an output ULP, far inside the 0.5 ULP margin, and the value is rounded once (half-to-even) at the very end.
This holds across the whole SCALE range, including SCALE = 38,
because the guard-digit intermediate is wider than i128. Every
strict transcendental is cross-checked against the platform f64
implementation at D38<9> (where f64 is comfortably more precise
than the type's ULP) - see the in-crate tests.
All wide tiers (D56 / D76 / D114 / D153 / D230 / D307
under the wide umbrella; D461 / D615 under x-wide; D923 /
D1231 under xx-wide) ship the full strict transcendental
surface — every method has a *_strict form plus a mode-aware
*_strict_with(mode) sibling. Two alternate implementations are
also exposed: ln_strict_agm and exp_strict_agm use the
quadratically-convergent Brent–Salamin / Newton path that scales
better than the artanh / Taylor canonical at very high working
scales; the canonical paths remain the default until a bench at
the relevant working scale shows AGM winning. The accuracy
contract is the same ≤ 0.5 ULP at storage as D38.
Choosing the configuration¶
| You want… | Use |
|---|---|
| Bit-identical results everywhere; correct rounding | default (strict is on by default) |
| Max speed at the cost of platform-libm precision | fast |
| Per-call explicit choice in the same build | always available via *_strict / *_fast regardless of features |
no_std + alloc |
default works (strict is no_std-compatible) |