Standalone tool for measuring the accuracy of Eigen‘s vectorized math functions in units of ULP (Unit in the Last Place). Compares Eigen’s SIMD implementations against either MPFR (128-bit high-precision reference) or the standard C++ math library.
From the Eigen build directory:
cd build cmake .. cmake --build . --target ulp_accuracy
If MPFR and GMP are installed, the build automatically enables MPFR support (EIGEN_HAS_MPFR). Without them, only --ref=std is available.
sudo apt install libmpfr-dev libgmp-dev
./test/ulp_accuracy [options]
Options:
--func=NAME Function to test (required unless --list)
--lo=VAL Start of range (default: -inf)
--hi=VAL End of range (default: +inf)
--double Test double precision (default: float)
--step=EPS Sampling step: advance by (1+EPS)*nextafter(x)
(default: 0 = exhaustive; useful for double, e.g. 1e-6)
--threads=N Number of threads (default: all cores)
--batch=N Batch size for Eigen eval (default: 4096)
--ref=MODE Reference: 'std' (default) or 'mpfr'
--hist_width=N Histogram half-width in ULPs (default: 10)
--list List available functions
List all supported functions:
./test/ulp_accuracy --list
Exhaustive float test of sin against std (tests all ~4.28 billion finite floats):
./test/ulp_accuracy --func=sin
Float test against MPFR (more accurate reference, but slower):
./test/ulp_accuracy --func=sin --ref=mpfr
Double precision test with geometric sampling (exhaustive is impractical for double):
./test/ulp_accuracy --func=exp --double --step=1e-6
Test a specific range:
./test/ulp_accuracy --func=sin --lo=0 --hi=6.2832
The tool prints:
Example output:
Function: sin (float)
Range: [-inf, inf]
Representable values in range: 4278190082
Reference: MPFR (128-bit)
Threads: 32
Batch size: 4096
Results:
Values tested: 4278190081
Time: 529.04 seconds (8.1 Mvalues/s)
Max |ULP error|: 2
at x = -1.5413464e+38 (Eigen=-0.482218683, ref=-0.482218742)
Mean |ULP error|: 0.0874
Signed ULP error histogram [-10, +10]:
-2 : 51988 ( 0.001%)
-1 : 186805349 ( 4.366%)
0 : 3904475407 ( 91.265%)
1 : 186805349 ( 4.366%)
2 : 51988 ( 0.001%)
Range splitting: The input range is divided evenly across threads by splitting the linear ULP space.
Batched evaluation: Each thread fills batches of input values, evaluates them through Eigen's vectorized path (using Eigen::Array operations), and computes reference values one at a time.
ULP computation: IEEE 754 bit patterns are mapped to a linear integer scale where adjacent representable values are adjacent integers. The signed ULP error is the difference between Eigen's result and the reference on this scale. Special cases (NaN, infinity mismatches) report infinite error.
Result reduction: Per-thread statistics (max error, mean error, histogram) are merged after all threads complete.
| Category | Functions |
|---|---|
| Trigonometric | sin, cos, tan, asin, acos, atan |
| Hyperbolic | sinh, cosh, tanh, asinh, acosh, atanh |
| Exponential/Log | exp, exp2, expm1, log, log1p, log10, log2 |
| Error/Gamma | erf, erfc, lgamma |
| Other | logistic, sqrt, cbrt, rsqrt |
ulp_accuracy.cpp — Main tool: ULP computation, worker threads, CLI, result printingmpfr_reference.h — MPFR reference function wrappers and scalar conversion helpers--ref=std this takes ~50 seconds per function; with --ref=mpfr it takes ~500 seconds (10x slower).--step=1e-6 to sample ~2.88 billion values geometrically.