Compact rational storage#
Status: Open lead. Owner: unassigned.
The layout probe is complete. Lifetime design, implementation, and solver evaluation remain pending.
Investigate overlapping the small numerator and owned big-number pointer in
util::Rational. Keep this separate from changing the arithmetic algorithms
or adopting another rational library.
Current layout and allocation#
Rational’s fields occupy space
simultaneously: a 64-bit numerator, a 64-bit denominator and a
std::unique_ptr<mpq_class>. A null pointer selects the small representation;
those values allocate nothing. Big values use the pointed-to GMP rational.
On the local Linux x86-64 GCC 13.3.0/libstdc++ build, the default-deleter
unique_ptr occupies eight bytes, exactly like a raw pointer. It has no
reference count or separately allocated ownership control block. Replacing
it with a raw pointer while retaining the same three fields saves no space.
There is an allocation for the big mpq_class object itself, made by the
big-value constructor, in addition
to GMP’s backing storage for large integers. The
copy operations deep-copy that
object; they do not share it. A raw pointer to a separately allocated
mpq_class would retain this allocation and indirection while transferring
cleanup responsibility to handwritten code.
Measured candidate layouts#
The C++ layout probe reports:
Record |
Inline bytes |
|---|---|
Current |
24 |
Same fields with a raw pointer |
24 |
Union of small pair / owned pointer, plus separate tag |
24 |
Union of numerator / owned pointer, tagged by denominator |
16 |
Union of small pair / inline |
40 |
Current DeltaRational / candidate using compact rationals |
48 / 32 |
Current LraTableau::RowEntry / candidate using compact rational |
40 / 32 |
The separate-tag union has a 16-byte payload plus its tag and alignment
padding, so it saves nothing here. A smaller candidate uses the denominator
as the discriminator: small values already require denominator > 0, so
denominator == 0 can select a pointer in the overlapping payload:
denominator > 0: [ denominator ][ numerator ]
denominator = 0: [ 0 ][ unique_ptr to GMP ]
This uses an unused denominator value, not a pointer bit or a narrower small
number range. It can keep unique_ptr ownership. The potential saving is
eight inline bytes per rational, including small values. That is one third
of Rational’s inline size and one fifth of the illustrated row entry’s size;
it is not a measured reduction in solver memory or runtime.
Embedding mpq_class directly in the union would remove the allocation and
indirection for the wrapper object, but its 32-byte size enlarges every value
in this simple tagged layout to 40 bytes. GMP’s separately allocated integer
storage remains. The tradeoff depends on the small/big value distribution.
Reproduce the layout observation from the repository root:
c++ --version
c++ -std=c++20 -Wall -Wextra -Wpedantic -Werror \
-Isrc -isystem contrib/local/include \
docs/examples/rational-layout.cpp -o /tmp/primo-rational-layout
/tmp/primo-rational-layout
Candidate records are used only with sizeof; they are not working arithmetic
types. These sizes depend on the ABI and library implementation. The probe
does not run the solver or measure allocations, cache behavior or peak memory.
Design and evaluation still needed#
Before implementation, specify active-union-member construction and destruction,
copy/move operations, a valid moved-from state, self-assignment, and exception
safety during promotion and copying. A union containing unique_ptr needs
explicit lifetime management even though the pointer still owns deletion.
Preserve reduced small fractions, automatic demotion when a big result fits
small, and identical equality/hash behavior regardless of the arithmetic path.
Measure the resulting arrays and owners, not only the scalar type: tableau rows, infinitesimal values, bounds, undo records, core terms and interning keys all contain rational values. Coordinate this with the separate term-storage lead. The retained D5 library-replacement investigation addresses a different way of reducing ownership obligations.
A prototype needs focused lifetime and promotion/demotion regressions, sanitizers, the solver correctness gates, and comparisons covering both small-value-heavy and big-value-heavy inputs. Compare allocation counts, peak/retained memory and total runtime. Keep the current layout if the ownership complexity or runtime regressions outweigh the memory saving.
Contributors may use any suitable machine. In the maintainer workspace, these batches run on SLURM; see the execution policy.