Extended Rationals — Qx32 / Qx64 / Qx128 / Qx256 / Qx512

XRational32 (Qx32), XRational64 (Qx64), XRational128 (Qx128), XRational256 (Qx256), and XRational512 (Qx512) combine IEEE-like special values with lazy GCD normalization for maximum throughput. Overflow saturates to Inf or NaN instead of throwing.

When to use

  • You need IEEE-like robustness: Inf, -Inf, NaN propagation, and overflow saturation.
  • You need maximum arithmetic throughput for chains of operations.
  • You want zero-allocation arithmetic with special-value support.

Advantages

  • 3-13x faster than Rational{Int} for chained arithmetic.
  • Never throws on arithmetic: overflow saturates to Inf/NaN, division by zero returns Inf.
  • IEEE semantics: NaN propagates, Inf arithmetic follows expected rules, NaN sorts last.
  • Zero allocation: Qx32 uses Int64 intermediates, Qx64 uses Int128, Qx128 uses Int256, Qx256 uses Int512, and Qx512 uses Int1024.
  • Correct equality: cross-multiplication comparison works without normalization.

How lazy normalization works

Standard rational types compute gcd(|num|, den) after every operation. Qx32/Qx64/Qx128/Qx256/Qx512 skip this step — they store the result with den > 0 and correct sign, but may leave a common factor. Normalization happens only when needed:

  • Display (show, print): normalizes before printing.
  • Hashing (hash): normalizes so equal values hash identically.
  • Accessors (numerator, denominator): return the canonical form.
  • Conversion to other types: normalizes first.

Arithmetic and comparisons never normalize:

  • == uses cross-multiplication: a.num * b.den == b.num * a.den
  • < uses cross-multiplication: a.num * b.den < b.num * a.den
  • +, -, *, / compute in wider integers and store the raw result.

Special-value encoding

Valuenumden
NaN00
+Inf10
-Inf-10

Construction

using XRationals

a = Qx32(2, 3)         # 2//3
b = Qx64(355, 113)     # 355//113
c = Qx128(170141183460469231731687303715884105727, 3)
d = Qx256(7, 3)
e = Qx512(7, 3)

# Special values
Qx32(1, 0)              # Inf
Qx32(-1, 0)             # -Inf
Qx32(0, 0)              # NaN

# From floats
Qx64(3.14)              # best Int64 rational approximation
Qx128(3.14)             # best Int128 rational approximation
Qx256(3.14)             # best Int256 rational approximation
Qx512(3.14)             # best Int512 rational approximation

# typemin is rejected
Qx32(typemin(Int32), 1) # throws OverflowError
Qx128(typemin(Int128), 1) # throws OverflowError
Qx512(typemin(Int512), 1) # throws OverflowError

Arithmetic with saturation

a = Qx32(2, 3)
b = Qx32(5, 7)

a + b    # 29//21
a * b    # 10//21
a ^ 3    # 8//27

# Overflow saturates
Qx32(typemax(Int32), 1) + 1          # Inf
Qx64(typemin(Int64) + 1, 1) - 1      # -Inf
Qx128(typemax(Int128), 1) + 1        # Inf

# Division by zero
Qx32(1, 2) / Qx32(0, 1)             # Inf
Qx32(0, 1) / Qx32(0, 1)             # NaN

Lazy storage, correct equality

# Stored unnormalized internally
x = Qx32(6, 8)    # stores num=6, den=8 (not reduced)

# Display normalizes
sprint(show, x)    # "3//4"

# Equality uses cross-multiply — no GCD needed
x == Qx32(3, 4)   # true
x == Qx32(9, 12)  # true

# numerator/denominator return canonical form
numerator(x)       # 3
denominator(x)     # 4

Chained operations

Each intermediate result skips GCD — this is where the speedup compounds:

a = Qx64(2, 3)
b = Qx64(5, 7)
c = Qx64(3, 13)
d = Qx64(11, 7)

a + b + c + d    # 869//273 (zero GCDs computed)
a * b - c * d    # 31//273

Inf and NaN propagation

inf  = Qx32(1, 0)
ninf = Qx32(-1, 0)
nan  = Qx32(0, 0)

inf + Qx32(5, 1)    # Inf
inf + ninf           # NaN (indeterminate)
inf * Qx32(0, 1)    # NaN (indeterminate)
nan + Qx32(1, 1)    # NaN (propagates)

Ordering

Ordering follows IEEE conventions: NaN is unordered and sorts last.

vals = [Qx32(3, 2), Qx32(-1, 2), Qx32(1, 0), Qx32(-1, 0), Qx32(0, 1)]
sort(vals)   # [-Inf, -1//2, 0//1, 3//2, Inf]

sort([Qx32(0, 0), Qx32(1, 1), Qx32(-1, 1)])   # [-1//1, 1//1, NaN]

Fused multiply-add

fma(x, y, z) normalizes operands first, then uses exact intermediate precision:

  • Qx32: intermediate in Int64, result via Stern-Brocot in Int128
  • Qx64: intermediate in Int128, result via Stern-Brocot in Int256
  • Qx128: intermediate in Int256, result via Stern-Brocot in Int512/Int1024
  • Qx256: finite arguments are routed through Rational{Int256} and converted back into Qx256
  • Qx512: finite arguments are routed through Rational{Int512} and converted back into Qx512

Use muladd(x, y, z) (which is just x*y + z) for the fast path when you don't need the exact intermediate guarantee.

fma(Qx32(2, 3), Qx32(3, 4), Qx32(1, 2))   # 1//1

# muladd is faster (lazy, no intermediate normalization)
muladd(Qx32(2, 3), Qx32(3, 4), Qx32(1, 2)) # 1//1

Cross-width conversion

Widen smaller extended rationals exactly through constructor-first APIs, or narrow wider ones to the nearest representable smaller type. widen exposes the same widening ladder at the type and value levels.

wide = Qx64(355, 113)
narrow = Qx32(wide)      # 355//113 (fits exactly)

raw32 = Qx32(6, 8)
Qx64(raw32)              # exact widening, preserves the raw 6//8 storage
Qx128(raw32)             # exact widening into Int128-backed storage
Qx256(raw32)             # exact widening into Int256-backed storage
Qx512(raw32)             # exact widening into Int512-backed storage
Qx128(wide)              # exact widening from Qx64
Qx256(wide)              # exact widening from Qx64
Qx512(wide)              # exact widening from Qx64
wide128 = Qx128(wide)
Qx256(wide128)           # exact widening from Qx128
Qx512(wide128)           # exact widening from Qx128
wide256 = Qx256(wide128)
Qx512(wide256)           # exact widening from Qx256
convert(Qx64, raw32)     # same widening semantics as the constructor
widen(Qx32)              # Qx64
widen(raw32)             # same as Qx64(raw32)
widen(Qx64)              # Qx128
widen(wide)              # same as Qx128(wide)
widen(Qx128)             # Qx256
widen(wide128)           # same as Qx256(wide128)
widen(Qx256)             # Qx512
widen(wide256)           # same as Qx512(wide256)

huge = Qx64(typemax(Int64), 1)
Qx32(huge)                # Inf (saturates)

Qx64(Qx128(1, Int128(2) * Int128(typemax(Int64)) + 1))  # 0//1 (nearest Qx64)
Qx32(Qx128(1, Int128(2) * Int128(typemax(Int32)) + 1))  # 0//1 (nearest Qx32)
Qx128(Qx256(Qx128(7, 3)))                                # 7//3 (fits exactly)
Qx256(Qx512(Qx256(7, 3)))                                # 7//3 (fits exactly)

Performance

Typical speedups over Rational{Int} (minimum nanoseconds, zero allocations):

Run the full benchmark harness with:

julia --project=. test/Benchmark.jl

Qx32 vs Rational{Int32}

OperationRational{Int32}Qx32Speedup
a + b13 ns2 ns7.0x
a * b8 ns2 ns4.2x
a / b7 ns2 ns3.6x
muladd(a,b,a)25 ns3 ns7.5x
a+b+c+d66 ns5 ns14.3x
a*b-c*d40 ns4 ns10.1x

Qx64 vs Rational{Int64}

OperationRational{Int64}Qx64Speedup
a + b14 ns3 ns5.3x
a * b9 ns2 ns4.0x
a / b8 ns3 ns3.2x
muladd(a,b,a)28 ns6 ns4.6x
a+b+c+d72 ns8 ns9.4x
a*b-c*d43 ns5 ns8.2x

Qx128 vs Rational{Int128}

OperationRational{Int128}Qx128Speedup
a + b75 ns7 ns10.4x
a * b65 ns6 ns10.8x
a / b60 ns7 ns8.9x
muladd(a,b,a)144 ns12 ns11.7x
a+b+c+d269 ns20 ns13.2x
a*b-c*d216 ns17 ns12.5x

Qx256 vs Rational{Int256}

OperationRational{Int256}Qx256Speedup
a + b267 ns23 ns11.4x
a * b230 ns16 ns14.3x
a / b208 ns19 ns11.1x
muladd(a,b,a)438 ns38 ns11.4x
a+b+c+d996 ns69 ns14.5x
a*b-c*d791 ns53 ns14.8x

Qx512 vs Rational{Int512}

OperationRational{Int512}Qx512Speedup
a + b583 ns93 ns6.3x
a * b461 ns59 ns7.9x
a / b417 ns64 ns6.5x
muladd(a,b,a)941 ns156 ns6.0x
a+b+c+d2.1 us310 ns6.8x
a*b-c*d1.7 us222 ns7.9x

As in the README benchmark notes, fma is the exception: XRationals routes finite fused multiply-add through a slower exact-or-canonical path before converting back to the fixed-width result, so muladd is the faster choice when you do not need that guarantee.

Width summary

TypeBackingLazy arithmetic intermediatesBest fit for
Qx32Int32Int64Small hot loops and compact storage
Qx64Int64Int128General-purpose exact rational work
Qx128Int128Int256Much larger finite range with the same IEEE-like semantics
Qx256Int256Int512Very large exact ranges with the same IEEE-like semantics
Qx512Int512Int1024Widest fixed-width exported range with the same IEEE-like semantics

Predicates

x = Qx32(3, 4)

isfinite(x)       # true
isinf(Qx32(1,0))  # true
isnan(Qx32(0,0))  # true
iszero(Qx32(0,1)) # true
isone(Qx32(1,1))  # true
isinteger(Qx32(4,1))  # true
signbit(Qx32(-3,4))   # true