The tests added to this fork (tests/elementary.cpp, commit 0e2504f) found bounds of GAOL's hyperbolic functions that do not enclose the exact values. On Linux x86_64 with glibc 2.31, it happens for asinh(-0x1.ee84df02a8766p-4) and acosh(0x1.01fd62fff333fp+0). Two fixes are ready, one on master and one on the branch hyperbolic-rigorous. This issue is to choose between them.
Why the bounds are wrong
mathlib, which bounds GAOL's other elementary functions, has no hyperbolic function; crlibm has sinh and cosh only. GAOL therefore takes sinh, cosh, tanh, asinh, acosh and atanh from the libm of the system, rounded to nearest, and moves the value one float outward (gaol_double_op_apmathlib.h, gaol_double_op_crlibm.h).
That gives a bound only when the libm returns one of the two doubles around the exact value, and libms are not always that accurate. On 2000 random arguments of each function (exact values computed by mpmath with 2000 bits), the libm returned a double beyond those two this many times:
| libm |
sinh |
cosh |
tanh |
asinh |
acosh |
atanh |
| glibc 2.31, x86_64 |
2 |
0 |
86 |
0 |
9 |
2 |
| musl, x86_64 and aarch64 |
2 |
3 |
80 |
0 |
9 |
2 |
| MinGW-w64, x64 (under Wine) |
2 |
0 |
86 |
0 |
9 |
2 |
| MinGW-w64, x86 (under Wine) |
0 |
0 |
0 |
0 |
0 |
0 |
| glibc 2.44, aarch64 and armv7 |
0 |
0 |
0 |
0 |
0 |
0 |
The libm was never more than one float beyond, but each of these times GAOL's bound on that side did not enclose the exact value.
Codac calls gaol::sinh(), gaol::cosh(), etc. for its intervals, and builds GAOL from the official repository, so it gets the same bounds.
Solution 1: move the libm values 3 floats outward (master, commit fdabc12)
- Change: two helpers, called instead of
previous_float() and next_float() in the functions taken from the libm (gaol_double_op_apmathlib.h, and tanh, acosh, asinh and atanh in gaol_double_op_crlibm.h).
- Guarantee: the bounds enclose the exact values as long as the libm is within two floats of them. That is still an assumption about the libm. The tests check it on each platform of the continuous integration, but it is not proven.
- Tightness: bounds 3 floats from the tightest ones, sometimes 2 or 4.
- Speed: 30 to 60 ns more per call, for four more
nextafter().
Solution 2: bound them without the libm (branch hyperbolic-rigorous, commit ead154d)
- Change: about 300 lines in
gaol_interval.cpp, computed in interval arithmetic with exp_dn(), exp_up(), log_dn() and log_up():
sinh, cosh: Taylor series below 1 (the terms after the tenth are bounded), (e^x ∓ e^-x)/2 up to 20, then e^x/2 and one float beyond;
tanh: sinh/cosh below 1, 1 - 2/(e^2x + 1) above;
asinh, acosh, atanh: the libm value is only a starting point. Each bound is the nearest double that the enclosures of sinh, of cosh - 1 = 2 sinh(x/2)^2 and of tanh prove to be a bound, searched double by double. A formula with log() is used when the libm is more than 16 doubles away.
- Near 0 and beyond 2^30, the first terms of the series and of the asymptotic expansions give the bounds.
- Guarantee: it rests only on mathlib (or crlibm) bounding
exp and log, as GAOL's other functions do.
- Tightness: bounds 0 to 3 floats from the tightest ones (4 once for
sinh), 0.3 to 1.9 on average.
- Speed: 6 to 45 times slower.
Both pass all the tests of tests/. Neither changes the variant that uses only the libm (gaol_double_op_m.h, without mathlib or crlibm).
Measurements
2000 random arguments of each function; Linux x86_64, glibc 2.31, GCC 9, -O2.
Bounds that do not enclose the exact value:
|
sinh |
cosh |
tanh |
asinh |
acosh |
atanh |
| GAOL (libm, 1 float) |
2 |
0 |
86 |
0 |
9 |
2 |
| Solution 1 (libm, 3 floats) |
0 |
0 |
0 |
0 |
0 |
0 |
| Solution 2 (without the libm) |
0 |
0 |
0 |
0 |
0 |
0 |
Mean number of floats between the bounds and the tightest ones (largest in parentheses):
|
sinh |
cosh |
tanh |
asinh |
acosh |
atanh |
| GAOL (libm, 1 float) |
0.99 (1) |
0.99 (1) |
0.98 (1) |
1 (1) |
1 (1) |
1 (1) |
| Solution 1 (libm, 3 floats) |
2.99 (4) |
2.99 (3) |
3.02 (4) |
3 (3) |
3.00 (4) |
3.00 (4) |
| Solution 2 (without the libm) |
0.29 (4) |
0.29 (2) |
1.02 (3) |
1.88 (3) |
1.63 (3) |
1.08 (3) |
Time per call on a point interval, in ns (best of 3 runs of 10^6 calls). Arguments are in [-5, 5] for sinh, cosh and tanh, [-100, 100] for asinh, [1, 100] for acosh, and [-0.99, 0.99] for atanh.
|
sinh |
cosh |
tanh |
asinh |
acosh |
atanh |
| GAOL (libm, 1 float) |
87 |
59 |
90 |
77 |
63 |
103 |
| Solution 1 (libm, 3 floats) |
145 |
89 |
144 |
121 |
108 |
144 |
| Solution 2 (without the libm) |
519 |
513 |
728 |
1131 |
1491 |
4522 |
To decide
The tests added to this fork (
tests/elementary.cpp, commit 0e2504f) found bounds of GAOL's hyperbolic functions that do not enclose the exact values. On Linux x86_64 with glibc 2.31, it happens forasinh(-0x1.ee84df02a8766p-4)andacosh(0x1.01fd62fff333fp+0). Two fixes are ready, one onmasterand one on the branchhyperbolic-rigorous. This issue is to choose between them.Why the bounds are wrong
mathlib, which bounds GAOL's other elementary functions, has no hyperbolic function; crlibm has
sinhandcoshonly. GAOL therefore takessinh,cosh,tanh,asinh,acoshandatanhfrom the libm of the system, rounded to nearest, and moves the value one float outward (gaol_double_op_apmathlib.h,gaol_double_op_crlibm.h).That gives a bound only when the libm returns one of the two doubles around the exact value, and libms are not always that accurate. On 2000 random arguments of each function (exact values computed by mpmath with 2000 bits), the libm returned a double beyond those two this many times:
The libm was never more than one float beyond, but each of these times GAOL's bound on that side did not enclose the exact value.
Codac calls
gaol::sinh(),gaol::cosh(), etc. for its intervals, and builds GAOL from the official repository, so it gets the same bounds.Solution 1: move the libm values 3 floats outward (
master, commit fdabc12)previous_float()andnext_float()in the functions taken from the libm (gaol_double_op_apmathlib.h, andtanh,acosh,asinhandatanhingaol_double_op_crlibm.h).nextafter().Solution 2: bound them without the libm (branch
hyperbolic-rigorous, commit ead154d)gaol_interval.cpp, computed in interval arithmetic withexp_dn(),exp_up(),log_dn()andlog_up():sinh,cosh: Taylor series below 1 (the terms after the tenth are bounded),(e^x ∓ e^-x)/2up to 20, thene^x/2and one float beyond;tanh:sinh/coshbelow 1,1 - 2/(e^2x + 1)above;asinh,acosh,atanh: the libm value is only a starting point. Each bound is the nearest double that the enclosures ofsinh, ofcosh - 1 = 2 sinh(x/2)^2and oftanhprove to be a bound, searched double by double. A formula withlog()is used when the libm is more than 16 doubles away.expandlog, as GAOL's other functions do.sinh), 0.3 to 1.9 on average.Both pass all the tests of
tests/. Neither changes the variant that uses only the libm (gaol_double_op_m.h, without mathlib or crlibm).Measurements
2000 random arguments of each function; Linux x86_64, glibc 2.31, GCC 9,
-O2.Bounds that do not enclose the exact value:
Mean number of floats between the bounds and the tightest ones (largest in parentheses):
Time per call on a point interval, in ns (best of 3 runs of 10^6 calls). Arguments are in [-5, 5] for sinh, cosh and tanh, [-100, 100] for asinh, [1, 100] for acosh, and [-0.99, 0.99] for atanh.
To decide
masteras it ismasterby the commit ofhyperbolic-rigorous