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.
What's wrong
Several functions add or subtract
1(or1/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 by10^|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).Exp2M1andExp10M1go through it.LogP1:Log(Add(One, x))(Exponentials.cs, about line 320).Log2P1andLog10P1go through it.Sinh/Cosh: add or subtractraisedand1/raised(PreciseNumber.Hyperbolics.cs, about lines 110 and 153). The gap is about 2·|x|/ln 10.Asinh/Acosh:Add(One, square)andAdd(x, One)(Hyperbolics.cs, about lines 259 and 323-325).Reproduction (current
main, Debug, default precision)Correctness:
ExpM1(-1e10)throwsOverflowException: A result scaled by 10^-4342944819 needs an exponent outside the range of an int.The exact answer rounds to-1at any precision.Tanh(±1e10)already saturates to±1correctly, in about 30 ms.Exp2M1andExp10M1throw the same way at similar magnitudes.Performance:
Exp(-1e6)takes 3 ms, whileExpM1(-1e6)takes 442 ms and returns-1. The cost grows roughly quadratically with the exponent gap:ExpM1(-1e7)-1Exp(1e7): 41 msExp10M1(-1e7)-1ExpM1(-1e8)Sinh(1e7),Cosh(-1e7)LogP1(1e1000000)Log(1e1000000): 24 ms, same digitsAsinh(1e1000000)Acosh(1e1000000)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)orLog(x)to the requested precision.Suggested fix / acceptance criteria
Short-circuit when the exponent gap exceeds
significantDigitsplus the guard digits. At that point the±1term can't affect the rounded result.ExpM1:-1whenx < -(significantDigits + guard) · ln 10.Exp(x)whenExp(x)has a decimal magnitude of at leastsignificantDigits + guard.LogP1:Log(x)whenxis that large.xrounded when|x|is that small. The existing small-x path may already cover this.Sinh/Cosh: drop the1/raisedterm when it is below the precision.Asinh/Acosh: useln(2|x|)(with sign) for large|x|.Tests:
ExpM1(-1e10)returns-1and doesn't throw.ExpM1(-1e7),Sinh(1e7)andLogP1(1e1000000)each finish in well under a second, with the same digits as today where today's call completes.