Skip to content

ExpM1(-1e10) throws OverflowException instead of returning -1, and ExpM1/LogP1/Sinh/Cosh/Asinh/Acosh take seconds to minutes for large arguments #133

Description

@matt-edmondson

What's wrong

Several functions add or subtract 1 (or 1/raised) exactly against an intermediate result whose exponent is very far from 0, and only round afterwards. CommonizeSignificands (PreciseNumber.cs, about line 923) aligns the two operands by scaling the significand by 10^|exponent gap|. That builds a BigInteger with millions of digits, and the rounding step then discards almost all of it. The call sites:

  • ExpM1: Subtract(Exp(x, wide), One) (PreciseNumber.Exponentials.cs, about line 489). Exp2M1 and Exp10M1 go through it.
  • LogP1: Log(Add(One, x)) (Exponentials.cs, about line 320). Log2P1 and Log10P1 go through it.
  • Sinh / Cosh: add or subtract raised and 1/raised (PreciseNumber.Hyperbolics.cs, about lines 110 and 153). The gap is about 2·|x|/ln 10.
  • Asinh / Acosh: Add(One, square) and Add(x, One) (Hyperbolics.cs, about lines 259 and 323-325).

Reproduction (current main, Debug, default precision)

Correctness: ExpM1(-1e10) throws OverflowException: A result scaled by 10^-4342944819 needs an exponent outside the range of an int. The exact answer rounds to -1 at any precision. Tanh(±1e10) already saturates to ±1 correctly, in about 30 ms. Exp2M1 and Exp10M1 throw the same way at similar magnitudes.

Performance: Exp(-1e6) takes 3 ms, while ExpM1(-1e6) takes 442 ms and returns -1. The cost grows roughly quadratically with the exponent gap:

Call Time Same-magnitude baseline
ExpM1(-1e7) 9.9 s, result -1 Exp(1e7): 41 ms
Exp10M1(-1e7) 45 s, result -1
ExpM1(-1e8) over 120 s (killed)
Sinh(1e7), Cosh(-1e7) about 43 s each, about 280 MB allocated
LogP1(1e1000000) 13 s Log(1e1000000): 24 ms, same digits
Asinh(1e1000000) 47 s
Acosh(1e1000000) 40 s

An arbitrary-precision library will receive large or tiny arguments from user input or from generic IExponentialFunctions<T> code. Any of these calls can block a thread for minutes, or throw, where the answer is -1, Exp(x) or Log(x) to the requested precision.

Suggested fix / acceptance criteria

Short-circuit when the exponent gap exceeds significantDigits plus the guard digits. At that point the ±1 term can't affect the rounded result.

  • ExpM1:
    • Return -1 when x < -(significantDigits + guard) · ln 10.
    • Return Exp(x) when Exp(x) has a decimal magnitude of at least significantDigits + guard.
  • LogP1:
    • Use Log(x) when x is that large.
    • Return x rounded when |x| is that small. The existing small-x path may already cover this.
  • Sinh / Cosh: drop the 1/raised term when it is below the precision.
  • Asinh / Acosh: use ln(2|x|) (with sign) for large |x|.
  • A more general alternative: add a helper that adds two values rounded to a given precision. When one operand is below the other's last kept digit, it skips alignment and applies only the sticky or rounding effect.

Tests:

  • ExpM1(-1e10) returns -1 and doesn't throw.
  • ExpM1(-1e7), Sinh(1e7) and LogP1(1e1000000) each finish in well under a second, with the same digits as today where today's call completes.

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

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions