Skip to content
Select theme

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.

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:

untyped 1.0: 8 bytes
1.0 as f32: 4 bytes

An 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 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⁵³:

f32 sees 16777217.0 as 16777216
f64 sees 9007199254740993.0 as 9007199254740992

Neither 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.

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:

0.1 + 0.2 == 0.3 in f64: false

This 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.

Like the integer types, f32 and f64 never meet in one expression. There is no implicit conversion anywhere in Rust:

Terminal window
cargo run -q
error[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 errors

Widening — 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:

widened: 0.20000000149011612

Look 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.

  • Bulk numeric data. A Vec<f32> of a million samples is 4 MB; the same Vec<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 in f32 avoids a conversion at every boundary.
  • Everything else: f64. On modern x86-64 and ARM hardware, f64 arithmetic is not meaningfully slower than f32 — 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.