Fundamentals 11 min read

Why 0.1 + 0.2 ≠ 0.3: A Deep Dive into Floating‑Point Precision and the Ryū Algorithm

The article explains why adding 0.1 and 0.2 in Rust (and other IEEE‑754 languages) yields 0.30000000000000004, explores the binary representation limits of f64, introduces the ULP concept, and details how the Ryū algorithm produces the shortest correct decimal string for floating‑point numbers.

Rust High-Frequency Quantitative Trading
Rust High-Frequency Quantitative Trading
Rust High-Frequency Quantitative Trading
Why 0.1 + 0.2 ≠ 0.3: A Deep Dive into Floating‑Point Precision and the Ryū Algorithm

From a Rust perspective the classic example 0.1_f64 + 0.2_f64 prints 0.30000000000000004, a behavior shared by Python, JavaScript, Java, Go and any language that uses IEEE‑754 floating‑point numbers.

The article asks why 0.3_f64 prints the clean string "0.3" while the sum prints an ugly one, even though both values appear "almost the same".

Rust’s f64::round() only rounds to an integer. To keep n decimal places the common "multiply‑round‑divide" trick is shown:

fn round_to(value: f64, decimals: u32) -> f64 {
    let factor = 10_f64.powi(decimals as i32);
    (value * factor).round() / factor
}

fn main() {
    let x = 0.1 + 0.2;
    let r = round_to(x, 1);
    println!("{}", r);            // 0.3
    println!("{}", r.to_string()); // 0.3
}

Although the printed result looks correct, the underlying f64 value is still an approximation.

f64 stores numbers in binary (1 sign bit, 11 exponent bits, 52 mantissa bits). The exact value of 0.3_f64 is: 0.29999999999999998889776975374843459576368... You can verify this with:

println!("{:.20}", 0.3_f64); // 0.29999999999999999000

Because 0.3 cannot be represented exactly, the printed string is produced by a conversion algorithm, not by the stored bits.

The article introduces the Unit in the Last Place (ULP) – the distance between two adjacent representable floating‑point numbers. ULP size varies with magnitude; near 1.0 one ULP ≈ 2.2e‑16, near 1 000 000 one ULP ≈ 1.2e‑10. Example code demonstrates this:

let a = 1.0_f64;
let next_a = f64::from_bits(a.to_bits() + 1);
println!("{:e}", next_a - a); // 2.220446049250313e-16

let b = 1_000_000.0_f64;
let next_b = f64::from_bits(b.to_bits() + 1);
println!("{:e}", next_b - b); // 1.1641532182693481e-10

The sum 0.1_f64 + 0.2_f64 and the literal 0.3_f64 differ by exactly 1 ULP, which explains the different printed results:

println!("{}", (0.1_f64 + 0.2).to_bits() - 0.3_f64.to_bits()); // 1

To convert a floating‑point value to the shortest correct decimal string, the Ryū algorithm (Ulf Adams, 2018) satisfies three requirements: correctness (round‑trip), minimal length, and constant‑time speed. Its three steps are:

Determine the "legal interval" between the previous and next representable floats.

Convert the binary interval to decimal using a pre‑computed table of powers of 5 and a single 128‑bit multiplication.

Iteratively trim trailing digits while staying inside the legal interval, yielding the shortest representation.

For 0.3_f64 the legal interval includes the string "0.3", so Ryū outputs it. For the sum, the interval excludes "0.3" and includes "0.30000000000000004", which is why the printed result differs.

After rounding with round_to, the value’s bit pattern matches 0.3_f64, so Ryū again prints "0.3". The article warns that the rounded value is still an approximation; in domains requiring strict accuracy (e.g., finance) one should use decimal libraries such as rust_decimal or integer arithmetic.

The article surveys the evolution of floating‑point‑to‑string algorithms: Dragon4 (1990), its optimized C version dtoa.c (1996), Grisu3 (2010), Ryū (2018), Schubfach (2020) and Dragonbox (2020). It lists language adoption:

Rust’s standard library uses Ryū via the ryu crate.

Go 1.12+ uses Ryū in strconv.FormatFloat.

Swift 5.x adopts Ryū.

Java 17 switched from FloatingDecimal to Schubfach.

CPython 3.1+ uses dtoa.c (Dragon4); Python 2 printed longer strings.

V8 and SpiderMonkey use double-conversion (Grisu3) with a small fallback to Dragon4.

C++ {fmt} (and std::format) use Dragonbox from version 8.0, an optimization of Ryū.

In summary, the article breaks the problem into four layers: a practical rounding solution in Rust, the binary storage limitation of f64, the ULP‑based precision difference, and the Ryū algorithm that efficiently produces the shortest, round‑trip‑safe decimal representation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Rustprecisionfloating-pointRyū algorithmULP
Rust High-Frequency Quantitative Trading
Written by

Rust High-Frequency Quantitative Trading

Rust High-Frequency Quantitative Trading System

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.