Consider (abs, inv, +, -, *, //) over values of type Rational{I}
where I<:Union{Int16…Int128, BigInt}. A substantive time sink is renormalizing the (numerator, denominator) pairs: div each by their gcd.
Use two sorts of Rationals, (1) those known to be reduced to lowest terms and (2) those not known to be reduced to lowest terms. And avoid the gcd stuff as long as possible while protecting the integrity of the calculation. Doing this obtains an arithmetically performant realization of bounded Rationals.
These occurrences prompt renormalization:
(a) an arithmetic op overflows and an operand is not known to be reduced
- normalize the operand[s] to lowest terms and retry the arithmetic op
(b) a Rational value that is not known to be reduced is obtained from a store or a stream
- normalize the value and pass that along, now known to be reduced
(c) a Rational value that is not known to be in lowest terms is offered (written to a store, displayed)
- normalize the value and utilize that, so all presentation is of canonical Rational forms
This is effective.
Implementing the type this way:
struct FastRational{IntForRational, TeleologicalState}
numerator::IntForRational
denominator::IntForRational
end
# where
const RationalInt =
Union{Int8, Int16, Int32, Int64, Int128, BigInt}
const IsKnownToBeReduced = Val{:IsKnownToBeReduced}
const IsNotKnownToBeReduced = Val{:IsNotKnownToBeReduced}
const TeleologicalState =
Union{IsKnownToBeReduced, IsNotKnownToBeReduced}
works well with simple expressions
each NNx is from one expr on one machine
@btime (a_numer//a_denom) * (b_numer//b_denom))
- relative to Rational{I}: Int32 (25x), Int64 (10x), BigInt (4x), Int128 (2x)
I am concerned that real world use may make normalization churn.
Algorithms that use many magnitude comparisons with values not known to be in reduced form could/would cause those values to be repeatedly re-reduced. The struct is immutable and the reduced forms, once derived, often would not persist. It seems that the mechanism above only allows values that are streamed out and later streamed in have their newly reduced state persist.
A much better way would be to have the representation allow for transitioning from the numerator and denominator values associated with a IsNotKnownToBeReduced
state to reduced numerator and denominator values associated with a IsNotKnownToBeReduced
state become (same referent, updated content) the reduced numerator and denominator values and morph its teleological state into IsKnownToBeReduced
.
How may this be accomplished, keeping performance?
Simply changing the struct
to a mutable struct
(no other edits) halves the performance gains.
Taking advantage of the mutability may win back some time, but those gains stick only if the teleological state becomes a third field (so it can be altered without breaking type
). Although adding to the struct size is not great for an otherwise juxtaposed primitive type pair.
Is there a clean way for that Bool field to govern dispatch as the parameterized version does … before runtime? It seems ad hock.