Hints — Module 19 (Floating-Point Arithmetic)
Graduated hints. Try the next one only after the previous fails you. They never give the whole solution.
Stage 1: Representation: pack, unpack, normalize
- The model is
value = (-1)^sign · frac · 2^expwithfraca 53-bit integer normalized so bit 52 is its leading 1. Everything reduces to splitting and reassembling anf64's 64 bits: 1 sign, 11 biased exponent, 52 mantissa. The implicit leading 1 is not stored — you add it on decode, strip it on encode. - Use
x.to_bits()/f64::from_bits(). For a normalf64(biased field in1..=2046):frac = (1<<52) | mantissaandexp = biased - 1075(since the value is1.m · 2^{biased-1023}and our exp is the weight offrac's last bit).normalizeleft-shiftsfrac(decrementingexp) untilfrac >= 2^52; zero staysfrac == 0. - Encode:
biased = exp + 1075,mantissa = frac & ((1<<52)-1), thenbits = (sign<<63) | (biased<<52) | mantissa.ulp(&self)is2^exp, i.e.Float::new(false, exp - 52, 1<<52). For the0.1test, note|10·frac - (1<<(-exp))| < 10is the "within one ulp of 1/10" check.
Stage 2: Addition and subtraction with rounding
- You cannot add significands until their exponents match. Align the smaller-exponent operand by shifting it right, but you must not lose bits that affect rounding: keep guard/round/sticky. Then add (or subtract, if signs differ), renormalize, and round to 53 bits, ties to even.
- Work in a
u128. Put the larger operand'sfracat a fixed high position by shifting left by aGUARD = 64bits; shift the smaller one right by the exponent difference. If that difference exceeds 64, the small operand can't reach the round bit — collapse it to a single sticky bit (frac != 0). Same sign → add the twou128s; opposite sign → subtract smaller magnitude from larger, and the result's sign is the larger's. - Factor rounding into one helper
round_wide(sign, exp, mag: u128): find the leading bitmsb = 127 - mag.leading_zeros(), setshift = msb - 52; ifshift <= 0shift left (exact); else round bit is(mag>>(shift-1))&1, sticky ismag & ((1<<(shift-1))-1) != 0, and round up iffround_bit==1 && (sticky || (kept&1)==1), handling carry-out to1<<53.subisself.add(&other.neg()).
Stage 3: Multiplication and division
- Multiplication: signs xor, exponents add, significands multiply. Two 53-bit
integers multiply to at most 106 bits, which fits exactly in a
u128— so the product is exact and only the final normalization rounds. Division's exact quotient is generally infinite, so you must generate guard/round/sticky yourself. mul:prod = (self.frac as u128) * (other.frac as u128), thenround_wide(sign, self.exp + other.exp, prod)(reuse stage 2's helper). Handle a zero operand up front, returning a signed zero.div: assertother.frac != 0with a message containing"division by zero". Computenum = (self.frac as u128) << 64,q = num / den,r = num % den; ifr != 0setq |= 1(the remainder is the sticky bit). Thenround_wide(sign, self.exp - other.exp - 64, q).
Stage 4: Error analysis: ulps and compensated summation
machine_epsilon()is2^{-52}— the gap between1.0and its successor.ulp_f64(x)is2^{exponent(x)-52}. Naive summation just folds with+; Kahan keeps a correction term for the bits lost each addition.ulp_f64: read the biased exponent fromx.abs().to_bits(), unbias (e = biased - 1023), return2^{e-52}(build the power viafrom_bitsorpowi).ulp_error(c, x) = |c - x| / ulp_f64(x).- Kahan:
s=0; c=0; for x { let y = x - c; let t = s + y; c = (t - s) - y; s = t; } s. The magic linec = (t - s) - yrecovers exactly the low bits thatt = fl(s+y)discarded; subtractingcnext iteration cancels them.