Skip to content

x + 0 costs time and memory proportional to x's exponent: 1e300000 + Zero takes ~1 s, and 1e2000000000 + Zero throws OutOfMemoryException #137

Description

@matt-edmondson

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.

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 working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions