Skip to content

Hyperbolic functions: move the libm values 3 floats outward, or bound them without the libm? #1

Description

@Jordan08

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

  • Solution 1: keep master as it is
  • Solution 2: replace the commit of solution 1 on master by the commit of hyperbolic-rigorous

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions