Skip to content

perf: custom f64 → BigRational conversion via mantissa/exponent decomposition #63

@acgetchell

Description

@acgetchell

Summary

Replace BigRational::from_float(x) with a manual mantissa/exponent decomposition that avoids GCD normalization, giving faster construction with identical exactness.

Current State

f64_to_bigrational (src/exact.rs:74) uses BigRational::from_float(x), which internally:

  1. Converts f64 to a rational
  2. Normalizes the rational (computes GCD of numerator and denominator, divides both)

The GCD reduction is unnecessary here because we can construct the exact rational directly from the IEEE 754 representation.

Proposed Changes

Implement a custom conversion that decomposes f64 into its IEEE 754 components:

fn f64_to_bigrational(x: f64) -> BigRational {
    // x = (-1)^sign × mantissa × 2^exponent
    // where mantissa is a 53-bit integer (for normals)
    let bits = x.to_bits();
    let sign = (bits >> 63) != 0;
    let biased_exp = ((bits >> 52) & 0x7FF) as i16;
    let frac = bits & 0x000F_FFFF_FFFF_FFFF;

    // Handle zero
    if biased_exp == 0 && frac == 0 {
        return BigRational::from_integer(0.into());
    }

    // Subnormal: mantissa = frac, exponent = 1 - 1023 - 52 = -1074
    // Normal: mantissa = (1 << 52) | frac, exponent = biased_exp - 1023 - 52
    let (mantissa, exponent) = if biased_exp == 0 {
        (frac, -1074_i16)
    } else {
        ((1u64 << 52) | frac, biased_exp - 1023 - 52)
    };

    let mut numer = BigInt::from(mantissa);
    let mut denom = BigInt::from(1u64);

    if exponent >= 0 {
        numer <<= exponent as u32;
    } else {
        denom <<= (-exponent) as u32;
    }

    if sign { numer = -numer; }
    BigRational::new_raw(numer, denom)
}

Benefits

  • No GCD computation on every f64 → rational conversion
  • Faster construction (direct bit manipulation + shift)
  • Identical exactness (every finite f64 is exactly m × 2^e)
  • More visible as numeric kernels get faster (conversion overhead becomes proportionally larger)

Implementation Notes

  • Must handle: zero, subnormals, normal values, negative values
  • Must NOT handle: NaN, infinity (callers validate finiteness before calling)
  • Verify BigRational::new_raw exists in num-rational (it does — creates ratio without normalization)
  • All existing tests must pass (especially subnormal test at line 637)
  • Depends on: perf: stack-backed storage for exact arithmetic kernels #62 (stack-backed storage, for combined benchmarking)
  • Blocks: Changed: Overhauls public API tests and examples #3 (integer Bareiss, which benefits from the mantissa/exponent decomposition)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperformancePerformance related issuesrustPull requests that update rust code

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions