liveBook · Manning

Floating-point arithmetic

As explained in Rust in Action: Systems programming concepts and techniques

A floating-point type represents real-number values using a finite-precision binary format with sign, exponent, and significand components.

Floating-point numbers let Rust work with values such as 1.0 and 42.42. Its f32 and f64 types occupy 32 and 64 bits. Unlike integers, these types also have bit patterns for infinity, negative infinity, and “not a number” (NaN). section 2.3.1 section 2.3.2

Think of scientific notation: a value has a sign, a significant part, and an exponent that sets its scale. A floating-point value stores these as a sign bit, exponent, and mantissa (also called a significand). Its base is fixed at 2, so it needs no separate base field. section 5.4

Listing 5.10 Deconstructing a floating-point value
 const BIAS: i32 = 127;                            #1
 const RADIX: f32 = 2.0;                           #1
 
 fn main() {                                       #2
   let n: f32 = 42.42;
 
   let (sign, exp, frac) = to_parts(n);
   let (sign_, exp_, mant) = decode(sign, exp, frac);
   let n_ = from_parts(sign_, exp_, mant);
 
   println!("{} -> {}", n, n_);
   println!("field    |  as bits | as real number");
   println!("sign     |        {:01b} | {}", sign, sign_);
   println!("exponent | {:08b} | {}", exp, exp_);
   println!("mantissa | {:023b} | {}", frac, mant);
 }
 
 fn to_parts(n: f32) -> (u32, u32, u32) {
   let bits = n.to_bits();
 
   let sign     = (bits >> 31) & 1;                #3
   let exponent = (bits >> 23) & 0xff;             #4
   let fraction =  bits & 0x7fffff ;               #5
 
   (sign, exponent, fraction)                      #6
 }
 
 fn decode(
   sign: u32,
   exponent: u32,
   fraction: u32
 ) -> (f32, f32, f32) {
   let signed_1 = (-1.0_f32).powf(sign as f32);    #7
 
   let exponent = (exponent as i32) - BIAS;        #8
   let exponent = RADIX.powf(exponent as f32);     #8
 
   for i in 0..23 {                                #9
     let mask = 1 << i;                            #9
     let one_at_bit_i = fraction & mask;           #9
     if one_at_bit_i != 0 {                        #9
       let i_ = i as f32;                          #9
       let weight = 2_f32.powf( i_ - 23.0 );       #9
       mantissa += weight;                         #9
     }                                             #9
   }                                               #9
 
   (signed_1, exponent, mantissa)
 }
 
 fn from_parts(                                    #10
   sign: f32,
   exponent: f32,
   mantissa: f32,
 ) -> f32 {
     sign *  exponent * mantissa
 }

Follow the 42.42 round trip: extract an f32’s three fields with to_bits() and bit operations, decode them, then combine their values again. section 5.4.5

Finite space means limited precision. Binary floating point cannot represent every decimal value exactly; even 0.1 has no exact binary representation. Consequently, equality tests on calculated results can surprise you: the supplied f32 example finds 0.1 + 0.2 equal to 0.3, while the f64 example does not. When appropriate, compare the absolute difference against a chosen tolerance instead of assuming exact equality. Also watch for mathematically undefined results. section 2.3.3

Why look inside the bits?

A large array of values confined to a small range, such as 0..=1, may not need the full range of f32 or f64. Extracting floating-point fields provides a starting point for designing a compact, one-byte numeric format. section 5.3 section 5.0

How Rust in Action: Systems programming concepts and techniques covers it

How deep it goes
Works from literals and comparison hazards down to bit-field extraction, decoding, and reconstruction of an f32 value.
Examples
Shows f32 and f64 comparisons of 0.1 + 0.2 with 0.3 and a bit-level round trip of 42.42.
What it assumes
Introduces numeric literals and binary notation before explaining floating-point fields through scientific notation and bit manipulation.
What you can do after
Build it from scratch by extracting floating-point fields and using them to design a limited-range, one-byte numeric type.
How it connects
Connects scientific notation, binary representation, numeric types, approximate equality, and data compression.

Also explained in

Compare how 20 books explain floating-point arithmetic →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Rust in Action: Systems programming concepts and techniques. The section links point to the parts of the book it draws on.