You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
x + 0 costs time and memory proportional to x's exponent: 1e300000 + Zero takes ~1 s, and 1e2000000000 + Zero throws OutOfMemoryException #137
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)
PreciseNumberx=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.
Priority: Medium. Only operands with very large exponents are affected, but the trigger is ordinary: a default/Zero accumulator, as the 2.0 migration guide recommends. The worst case is a thread blocked for seconds or an OutOfMemoryException, and the O(1) fix is small.
Area / suggested assignment:Add / Subtract / CommonizeSignificands in PreciseNumber/PreciseNumber.cs. Add a zero short-circuit next to the existing IsUnit fast path in Multiply.
Activity
matt-edmondson commentedon Sep 29, 2026
Triage
default/Zeroaccumulator, as the 2.0 migration guide recommends. The worst case is a thread blocked for seconds or anOutOfMemoryException, and the O(1) fix is small.Add/Subtract/CommonizeSignificandsinPreciseNumber/PreciseNumber.cs. Add a zero short-circuit next to the existingIsUnitfast path inMultiply.intfor two non-zero operands) but distinct. The same PR could reasonably land both.Generated by Claude Code