What's wrong
Adding or subtracting zero is not free. Zero (and default(PreciseNumber)) is stored at exponent 0, so Add and Subtract go through CommonizeSignificands in PreciseNumber/PreciseNumber.cs:
- It scales the other operand's significand by
10^exponent so that both operands are at exponent 0.
- That builds a BigInteger with
exponent + 1 digits.
- The sanitizing constructor then counts those digits and strips the trailing zeros again.
The result is just the operand, unchanged. Multiply already short-circuits zero and one (IsUnit), so x * 1 allocates nothing, but x + 0 still pays the full cost.
This hits ordinary code. The 2.0 migration guide recommends PreciseNumber total = default; to start an accumulator, so the first total += x pays this cost whenever x has a large positive exponent.
Reproduction (current main, net10.0)
PreciseNumber x = PreciseNumber.Parse("1e300000", CultureInfo.InvariantCulture);
_ = x + PreciseNumber.Zero; // or PreciseNumber.Zero + x, x - PreciseNumber.Zero, default(PreciseNumber) + x
_ = PreciseNumber.Parse("1e2000000000", CultureInfo.InvariantCulture) + PreciseNumber.Zero;
Observed (time, managed bytes allocated):
| Expression |
Time |
Allocated |
1e100000 + 0 |
216 ms |
3.7 MB |
0 + 1e100000 |
237 ms |
0.9 MB |
1e300000 + 0 |
938 ms |
13 MB |
0 + 1e300000 / 1e300000 - 0 / default + 1e300000 |
~1.1 s each |
3 MB |
1e300000 * 1 (for comparison) |
0 ms |
0 KB |
1e2000000000 + 0 |
OutOfMemoryException after 10 s |
|
Library functions that add a zero term are affected too: Hypot(1e1000000, 0) takes 25.8 s, although the answer is |x|.
Why it matters
The correct result is available in O(1): it is the other operand, or its negation for 0 - x. Today a zero term can block a thread for seconds, or crash with OOM, for a value that is otherwise perfectly representable. Generic-math code (T.Zero + …, Sum, accumulators) runs into this with no warning.
Suggested fix / acceptance criteria
- In
Add and Subtract, return the other operand (negated for 0 - x) when either significand is zero, before aligning exponents.
- Tests:
1e2000000000 + Zero, Zero + 1e2000000000, 1e2000000000 - Zero and Zero - 1e2000000000 return promptly with the right value.
- Adding zero to a value with a large exponent allocates nothing, in the same shape as the existing tests in
PreciseNumberValueTypeTests.
Related but distinct: #135 covers the exponent difference overflowing int for two non-zero operands. The zero case needs no alignment at all.
What's wrong
Adding or subtracting zero is not free.
Zero(anddefault(PreciseNumber)) is stored at exponent 0, soAddandSubtractgo throughCommonizeSignificandsinPreciseNumber/PreciseNumber.cs:10^exponentso that both operands are at exponent 0.exponent + 1digits.The result is just the operand, unchanged.
Multiplyalready short-circuits zero and one (IsUnit), sox * 1allocates nothing, butx + 0still pays the full cost.This hits ordinary code. The 2.0 migration guide recommends
PreciseNumber total = default;to start an accumulator, so the firsttotal += xpays this cost wheneverxhas a large positive exponent.Reproduction (current main, net10.0)
Observed (time, managed bytes allocated):
1e100000 + 00 + 1e1000001e300000 + 00 + 1e300000/1e300000 - 0/default + 1e3000001e300000 * 1(for comparison)1e2000000000 + 0OutOfMemoryExceptionafter 10 sLibrary functions that add a zero term are affected too:
Hypot(1e1000000, 0)takes 25.8 s, although the answer is|x|.Why it matters
The correct result is available in O(1): it is the other operand, or its negation for
0 - x. Today a zero term can block a thread for seconds, or crash with OOM, for a value that is otherwise perfectly representable. Generic-math code (T.Zero + …,Sum, accumulators) runs into this with no warning.Suggested fix / acceptance criteria
AddandSubtract, return the other operand (negated for0 - x) when either significand is zero, before aligning exponents.1e2000000000 + Zero,Zero + 1e2000000000,1e2000000000 - ZeroandZero - 1e2000000000return promptly with the right value.PreciseNumberValueTypeTests.Related but distinct: #135 covers the exponent difference overflowing
intfor two non-zero operands. The zero case needs no alignment at all.