dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
math.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_MATH_HPP
5#define DASMIG_CORE_MATH_HPP
6
7#include <array>
8#include <cfloat>
9#include <cmath>
10#include <cstddef>
11#include <limits>
12#include <numbers>
13
14/// @file math.hpp
15/// @brief Floating-point functions that give bit-identical results on every
16/// compiler, standard library and platform.
17/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
18///
19/// `std::exp` and `std::log` are not required to be correctly rounded, and
20/// glibc, the MSVC runtime, Apple's libm and Emscripten's musl differ in the
21/// last bit for some inputs. The functions here use only operations IEEE 754
22/// defines exactly (+, -, *, /, sqrt, floor, frexp, ldexp), so the same input
23/// gives the same bits everywhere. They are accurate to a few ulp, which is
24/// what sampling needs; they are not replacements for `<cmath>` in general.
25///
26/// Exactness also needs every operation rounded on its own. Compilers may fuse
27/// `a * b + c` into one FMA instruction (GCC in its default GNU mode, clang
28/// with `-ffp-contract=fast`, ARM64 targets), which rounds once instead of
29/// twice. Pragmas do not prevent this reliably (clang ignores them under
30/// `-ffp-contract=fast`), so every product that feeds an addition in the core
31/// passes through fenced(), which the compiler cannot see through. Fast-math
32/// modes, which also reassociate, are refused at compile time.
33
34#ifdef __FAST_MATH__
35#error "dasmig-core needs IEEE 754 arithmetic: do not compile with -ffast-math"
36#endif
37// clang-cl defines _M_FP_CONTRACT by default; fenced() covers clang.
38#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_FP_FAST) || defined(_M_FP_CONTRACT))
39#error "dasmig-core needs IEEE 754 arithmetic: do not compile with /fp:fast or /fp:contract"
40#endif
41#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1930
42#error "dasmig-core needs Visual Studio 2022 or later (earlier /fp:precise contracts)"
43#endif
44#if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD != 0
45#error "dasmig-core needs FLT_EVAL_METHOD == 0 (no x87 extended precision; use SSE2)"
46#endif
47
48namespace dasmig::core::math
49{
50
51/// @brief Return @p x unchanged, hiding its value from the optimizer.
52///
53/// Wrap a product in this before adding to it, so the compiler cannot fuse
54/// the two into a fused multiply-add: `fenced(a * b) + c`. Costs nothing on
55/// x86-64 and ARM64 (an empty asm statement); WebAssembly has no scalar FMA,
56/// and MSVC (VS2022, /fp:precise) does not contract, so neither needs it.
57///
58/// @param x A value, typically a product.
59/// @return @p x, rounded to double.
60[[nodiscard]] inline double fenced(double x) noexcept
61{
62#if (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(__i386__))
63 __asm__("" : "+x"(x)); // NOLINT(hicpp-no-assembler): empty optimization barrier
64#elif (defined(__GNUC__) || defined(__clang__)) && defined(__aarch64__)
65 __asm__("" : "+w"(x)); // NOLINT(hicpp-no-assembler): empty optimization barrier
66#elif defined(__wasm__) || (defined(_MSC_VER) && !defined(__clang__))
67#else
68 volatile double v = x;
69 x = v;
70#endif
71 return x;
72}
73
74/// @cond INTERNAL
75namespace detail
76{
77
78// ln(2) split so that k * ln2_hi is exact for |k| < 2^20 (ln2_hi has its low
79// 32 bits zero; ln2_hi + ln2_lo is ln(2) to about 2^-83).
80inline constexpr double ln2_hi = 6.93147180369123816490e-01; // NOLINT(modernize-use-std-numbers): deliberately not ln(2)
81inline constexpr double ln2_lo = 1.90821492927058770002e-10;
82inline constexpr double inv_ln2 = std::numbers::log2e;
83inline constexpr double sqrt_half = 7.07106781186547524401e-01;
84
85// Largest and smallest arguments for which exp is finite and non-zero.
86inline constexpr double exp_max = 7.09782712893383973096e+02;
87inline constexpr double exp_min = -7.45133219101941108420e+02;
88
89// 1/i! for i = 0..13: the Taylor series of exp on |r| <= ln(2)/2.
90inline constexpr std::size_t exp_terms = 14;
91inline constexpr std::array<double, exp_terms> exp_coeff = [] {
92 std::array<double, exp_terms> c{};
93 double factorial = 1.0;
94 double i = 0.0;
95 for (auto& coeff : c)
96 {
97 if (i > 0.0) factorial *= i;
98 coeff = 1.0 / factorial;
99 i += 1.0;
100 }
101 return c;
102}();
103
104// 2/(2k+1) for k = 0..9: log(m) = sum of these times s^(2k+1), s = (m-1)/(m+1),
105// for m in [sqrt(1/2), sqrt(2)), where |s| <= 0.1716.
106inline constexpr std::size_t log_terms = 10;
107inline constexpr std::array<double, log_terms> log_coeff = [] {
108 std::array<double, log_terms> c{};
109 double odd = 1.0;
110 for (auto& coeff : c)
111 {
112 coeff = 2.0 / odd;
113 odd += 2.0;
114 }
115 return c;
116}();
117
118} // namespace detail
119/// @endcond
120
121/// @brief Natural exponential, bit-identical on every platform.
122///
123/// Reduces x = k ln(2) + r with |r| <= ln(2)/2, evaluates a degree-13 Taylor
124/// polynomial in r by Horner's rule and scales by 2^k.
125///
126/// @param x Exponent.
127/// @return e^x; +inf above about 709.78, 0 below about -745.13, NaN for NaN.
128[[nodiscard]] inline double exp(double x) noexcept
129{
130 if (std::isnan(x)) return x;
131 if (x > detail::exp_max) return std::numeric_limits<double>::infinity();
132 if (x < detail::exp_min) return 0.0;
133
134 const double k = std::floor(fenced(x * detail::inv_ln2) + 0.5);
135 const double r = (x - fenced(k * detail::ln2_hi)) - fenced(k * detail::ln2_lo);
136
137 // Horner's rule from the highest coefficient down.
138 auto coeff = detail::exp_coeff.crbegin();
139 double p = *coeff;
140 for (++coeff; coeff != detail::exp_coeff.crend(); ++coeff)
141 {
142 p = fenced(p * r) + *coeff;
143 }
144 return std::ldexp(p, static_cast<int>(k));
145}
146
147/// @brief Natural logarithm, bit-identical on every platform.
148///
149/// Splits x = m 2^e with m in [sqrt(1/2), sqrt(2)), evaluates
150/// log(m) = 2 atanh((m-1)/(m+1)) as an odd series and adds e ln(2).
151///
152/// @param x Argument.
153/// @return ln(x); -inf for 0, +inf for +inf, NaN for negative or NaN.
154[[nodiscard]] inline double log(double x) noexcept
155{
156 if (std::isnan(x) || x < 0.0) return std::numeric_limits<double>::quiet_NaN();
157 if (x == 0.0) return -std::numeric_limits<double>::infinity();
158 if (std::isinf(x)) return x;
159
160 int e = 0;
161 double m = std::frexp(x, &e); // x = m * 2^e, m in [0.5, 1)
162 if (m < detail::sqrt_half)
163 {
164 m *= 2.0;
165 --e;
166 }
167 const double f = m - 1.0; // exact: m is within a factor of 2 of 1
168 const double s = f / (m + 1.0);
169 const double z = s * s;
170
171 auto coeff = detail::log_coeff.crbegin();
172 double p = *coeff;
173 for (++coeff; coeff != detail::log_coeff.crend(); ++coeff)
174 {
175 p = fenced(p * z) + *coeff;
176 }
177 const double log_m = fenced(s * p);
178 const auto k = static_cast<double>(e);
179 return fenced(k * detail::ln2_hi) + (log_m + fenced(k * detail::ln2_lo));
180}
181
182} // namespace dasmig::core::math
183
184
185#endif // DASMIG_CORE_MATH_HPP
double fenced(double x) noexcept
Return x unchanged, hiding its value from the optimizer.
Definition math.hpp:60
double log(double x) noexcept
Natural logarithm, bit-identical on every platform.
Definition math.hpp:154
double exp(double x) noexcept
Natural exponential, bit-identical on every platform.
Definition math.hpp:128