Why the weird summarysize results for Rational{BigInt}

julia> n = big"3"^100000;

julia> t = (n // 1, n, Rational{BigInt}(n));

julia> typeof(t)
Tuple{Rational{BigInt}, BigInt, Rational{BigInt}}

julia> map(Base.summarysize, t)
(19872, 19968, 20008)

julia> first(t) == last(t)
true

julia> n = big"3"^1000000;

julia> t = (n // 1, n, Rational{BigInt}(n));

julia> map(Base.summarysize, t)
(198184, 199272, 199312)

julia> first(t) == last(t)
true

julia> versioninfo()
Julia Version 1.9.0-DEV.1242
Commit 53bb7fb5df7 (2022-08-31 18:42 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × AMD Ryzen 3 5300U with Radeon Graphics
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.5 (ORCJIT, znver2)
  Threads: 3 on 8 virtual cores

When I have a BigInt, there’s two methods that come to mind for converting it into a rational:

  1. A direct invocation of the Rational constructor. This causes the summarysize compared to the original BigInt to increase.
  2. Creating a fraction with one as the denominator. This causes the summarysize to decrease in my examples.

I wonder what could be the reason for the disparity. Something to do with GMP’s allocation strategies? What are the performance implications here?

EDIT: it definitely seems like creating a rational in the second way causes the size of the BigInt numerator to decrease, something which doesn’t happen if I just use copy on the BigInt:

julia> n = big"3"^1000000;

julia> m = n//1;

julia> typeof(m.num)
BigInt

julia> typeof(m.den)
BigInt

julia> map(Base.summarysize, (n, m, m.num))
(199272, 198184, 198144)

Here is a clue

julia> n = big"3"^100000;

julia> n.alloc
2494

julia> Base.GMP.MPZ.set!(BigInt(), n).alloc
2477

julia> copy(n).alloc
2494

Note that copy(n::BigInt) dispatches to copy(x::Number) = x. There must be a reason for this, even though ismutable(big(1)) is true.

EDIT: If you follow the code for Base.GMP.MPZ.set! you’ll find that it allocates memory for the target based on the size of the source, not the alloc of the source:

julia> n.alloc
2494

julia> n.size
2477
void
mpz_set (mpz_ptr w, mpz_srcptr u)
{
  mp_ptr wp, up;
  mp_size_t usize, size;

  usize = SIZ(u);
  size = ABS (usize);

  wp = MPZ_NEWALLOC (w, size);

  up = PTR(u);

  MPN_COPY (wp, up, size);
  SIZ(w) = usize;
}

EDIT: It’s really here

julia> Rational(n).num.alloc
2494

julia> Rational(n, 1).num.alloc
2477

The two-arg version calls set, which as mentioned above, uses size rather than alloc to determine what to allocate.

And the one-arg version calls Base.unsafe_rational, which does no copy at all. The numerator in the rational is the same object as n.

1 Like

This seems inconsistent

julia> n = big(3);

julia> numerator(Rational(n, 1)) === n
false

julia> numerator(Rational(n)) === n
true

julia> real(Complex(n, 0)) === n
true

julia> real(Complex(n)) === n
true

Even the behavior for Rational alone is inconsistent.
The expected behavior for the constructors is not documented. I think this should be more predictable.

1 Like