What is the difference between f32 and f64?
Short answer: f64 is the default — write let x = 1.0; and you get an 8-byte double with about 15–16 significant decimal digits. f32 is the 4-byte single-precision type, good to about 7. Reach for f32 when memory is the point — millions of samples, GPU buffers, file formats that store single precision — and let f64 carry everything else, which is exactly what the language does.
The integer side of the same question — i32 versus usize — is How to choose between i32 and usize; floats end up with a different default.
What each one is
Section titled “What each one is”Both are IEEE 754 floating-point numbers; the difference is how many bits each spends. f64 gives 8 bytes to the value, f32 gives it 4 — and a bare 1.0 with no suffix and no annotation resolves to f64:
fn inspect<T>(name: &str, _: &T) { println!("{name}: {} bytes", std::mem::size_of::<T>());}
fn main() { let a = 1.0; // no suffix, no annotation let b: f32 = 1.0; // annotation (or the 1.0f32 suffix) opts into f32 inspect("untyped 1.0", &a); inspect("1.0 as f32", &b);}untyped 1.0: 8 bytes1.0 as f32: 4 bytesAn untyped float literal defaults to f64 the same way an untyped integer defaults to i32 — and notice the direction: for integers the default is the modest 4-byte type, but for floats the default is the bigger one. That is a deliberate choice by the language, and the next two sections are the reasons for it.
The precision ceiling
Section titled “The precision ceiling”The bits buy counting range. f32 has a 24-bit significand, so it holds every integer exactly up to 2²⁴ = 16,777,216 — and then stops. f64 has 53 bits, exact up to 2⁵³:
fn main() { let n = 16_777_217.0f32; // 2^24 + 1 println!("f32 sees 16777217.0 as {n}"); let m = 9_007_199_254_740_993.0f64; // 2^53 + 1 println!("f64 sees 9007199254740993.0 as {m}");}f32 sees 16777217.0 as 16777216f64 sees 9007199254740993.0 as 9007199254740992Neither type can see the last + 1: the literal does not fail, does not warn, it just rounds to the nearest value the type can hold. Between 2²⁴ and 2⁵³ there is a band where f64 counts perfectly but f32 has already started skipping whole numbers — an f32 holding a timestamp in milliseconds past that point, for instance, is quietly wrong.
Even f64 drops decimals
Section titled “Even f64 drops decimals”Counting integers is the easy case. 0.1 has no exact binary representation in either type — the question is only how much error each one keeps:
fn main() { let sum: f64 = 0.1 + 0.2; println!("0.1 + 0.2 == 0.3 in f64: {}", sum == 0.3);}0.1 + 0.2 == 0.3 in f64: falseThis is not a defect in f64; it is what binary floating point is. f64 just keeps enough bits that the error stays far below the 15-or-so digits it prints — while f32, with fewer than half the bits, accumulates visible drift much sooner. What to do about it when floats have to be compared is its own problem, and the chapter comes back to it later; the point here is that f64 buys margin, not exactness.
Mixing them does not compile
Section titled “Mixing them does not compile”Like the integer types, f32 and f64 never meet in one expression. There is no implicit conversion anywhere in Rust:
fn main() { let a: f64 = 0.1; let b: f32 = 0.1; println!("{}", a + b);}cargo run -qerror[E0308]: mismatched types --> src/main.rs:4:24 |4 | println!("{}", a + b); | ^ expected `f64`, found `f32`
error[E0277]: cannot add `f32` to `f64` --> src/main.rs:4:22 | 4 | println!("{}", a + b); | ^ no implementation for `f64 + f32` | = help: the trait `Add<f32>` is not implemented for `f64`help: the following other types implement trait `Add<Rhs>` --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/arith.rs:98:9 | 98 | const impl Add for $t { | ^^^^^^^^^^^^^^^^^^^^^ `f64` implements `Add`...113 | add_impl! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f16 f32 f64 f128 } | ---------------------------------------------------------------------------------- in this macro invocation | ::: /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/internal_macros.rs:22:9 | 22 | const impl $imp<$u> for &$t { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&f64` implements `Add<f64>`... 33 | const impl $imp<&$u> for $t { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `f64` implements `Add<&f64>`... 44 | const impl $imp<&$u> for &$t { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&f64` implements `Add` = note: this error originates in the macro `add_impl` (in Nightly builds, run with -Z macro-backtrace for more info)
Some errors have detailed explanations: E0277, E0308.For more information about an error, try `rustc --explain E0277`.error: could not compile `verify` (bin "verify") due to 2 previous errorsWidening — f32 to f64 — is lossless, because every f32 value is exactly representable as an f64. The as cast is safe here in a way it was not in the integer case:
fn main() { let a: f64 = 0.1; let b: f32 = 0.1; println!("widened: {}", a + b as f64);}widened: 0.20000000149011612Look closely at that output: the widening lost nothing, but the f32 rounding error rides along. 0.1 was already 0.100000001490116… inside f32, and the sum shows it. Going the other direction — f64 as f32 — rounds values that do not fit, the same one-way trap as has with integers.
When f32 is actually the right call
Section titled “When f32 is actually the right call”- Bulk numeric data. A
Vec<f32>of a million samples is 4 MB; the sameVec<f64>is 8 MB. At the scale where the difference matters, memory bandwidth — not arithmetic — is the cost, and halving the bytes pays twice. - Interfaces that speak single precision. GPU buffers, shader uniforms, audio samples, and plenty of file formats store
f32; keeping the pipeline inf32avoids a conversion at every boundary. - Everything else:
f64. On modern x86-64 and ARM hardware,f64arithmetic is not meaningfully slower thanf32— the price of the default is close to zero, while the price of running out of precision is a bug that only shows up at scale.
- Working with numbers — the rest of the chapter.