# Ryū and the fast path from float to decimal string in Rust

> Ryū is a Rust port of Ulf Adams' algorithm for converting floating point numbers to decimal strings, and its appeal is in the format it picks rather than only the speed: it will use scientific notation where the standard library never will. The caveats are that it is not the shortest representation, and that the dependency you need for formatting is not the one most Rust code already uses.

**dtolnay/ryu** — Fast floating point to string conversion

- Repository: https://github.com/dtolnay/ryu
- Stars: 709 · Forks: 39
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/dtolnay-ryu

## A line by line port of a paper with a proof attached

Ryū is described as a pure Rust implementation of Ryū, an algorithm to quickly convert floating point numbers to decimal strings, and the lineage is stated with unusual precision.

The PLDI'18 paper Ryū: fast float-to-string conversion by Ulf Adams includes a complete correctness proof of the algorithm, and the paper is available under the creative commons CC-BY-SA license. This Rust implementation is a line-by-line port of Ulf Adams' implementation in C.

That lineage explains the shape of the crate. A port of this kind is not a rewrite with a Rust flavoured API bolted on later; it is the same algorithm with the same lookup tables and the same reasoning, translated into safe Rust, plus a public API that is designed to be pleasant rather than to mirror the C surface.

The categories in the package manifest say the same thing from the packaging side. The crate is filed under `value-formatting`, and under `no-std` with `no-std::no-alloc`, so the code can be used in an environment without the standard library and without an allocator. The `rust-version` field is 1.71, and the edition is 2021, which makes it usable in a fairly wide range of existing codebases.

The optional `no-panic` dependency is there for a specific purpose, to let the compiler prove that the formatting routines cannot unwind, which matters if the crate is used somewhere a panic is not acceptable.

So the project is best understood as a careful, well-founded piece of low level engineering rather than a convenience wrapper, and the correctness argument is a peer reviewed paper rather than a test suite.

## The API is a reusable buffer and a format call

The public surface is small enough to describe in one example, which is how the documentation introduces it:

```rust
fn main() {
    let mut buffer = ryu::Buffer::new();
    let printed = buffer.format(1.234);
    assert_eq!(printed, "1.234");
}
```

The shape is the interesting part. `ryu::Buffer::new()` allocates a buffer once, and `buffer.format(value)` writes the result into it and returns a `&str` that borrows from the buffer. The buffer is reusable, so a hot loop formats many values without allocating per value.

The performance section names the same entry point in the context of measurement: the vertical axis in the benchmark chart shows nanoseconds taken by a single execution of `ryu::Buffer::new().format_finite(value)`, so a lower result indicates a faster library. The `format_finite` method is the one that does not have to handle the non-finite values, and the benchmark uses it because the inputs are uniform random bit patterns.

The repository layout shows what else is in the box. There is a `benches/` directory with a `bench` target using the `harness = false` setting, which is what criterion, the benchmarking framework in the dev dependencies, expects. There is an `examples/` directory holding `upstream_benchmark.rs`, which reproduces the upstream C benchmark against this implementation. There is a `fuzz/` directory, and the manifest excludes the fuzzing targets from the `not(miri)` dev dependencies, which is how cargo keeps the fuzzer's own dependencies from being required for a normal build.

The `build.rs` file is excluded from the published package, as are the PNG images, which is a small sign of care about what ends up in a published crate.

## The numbers the documentation actually claims

The performance claims are specific, and they are the kind you can check yourself, which is why the documentation tells you how.

First, the cross-implementation comparison. The dtoa-benchmark compares this library and other Rust floating point formatting implementations across a range of precisions.

Then the upstream comparison, with the commands to reproduce it. Upstream's benchmarks are run with Bazel:

```console
$ git clone https://github.com/ulfjack/ryu c-ryu
$ cd c-ryu
$ bazel run -c opt //ryu/benchmark:ryu_benchmark
```

and the same benchmark against the Rust implementation with cargo:

```console
$ git clone https://github.com/dtolnay/ryu rust-ryu
$ cd rust-ryu
$ cargo run --example upstream_benchmark --release
```

What those benchmarks measure is stated precisely: the average time to print a 32-bit float and average time to print a 64-bit float, where the inputs are distributed as uniform random bit patterns 32 and 64 bits wide. That is a fair distribution for a formatter, since it exercises subnormals and values with long digit strings rather than the tidy decimals a human would type.

The result claimed is that the upstream C code, the unsafe direct Rust port, and the safe pretty Rust API all perform the same, taking around 21 nanoseconds to format a 32-bit float and 31 nanoseconds to format a 64-bit float. The safe API matching the unsafe port is the point of the port, since the safety is supposed to be free.

Against the standard library, the claim is that Ryū is approximately 2-5x faster than the standard library across a range of f32 and f64 inputs, with measurements in nanoseconds per iteration where smaller is better. That benchmark is run with `cargo bench`.

The chart itself is an image in the repository, dtoa-benchmark.png, alongside performance.png, so the numbers are also presented visually.

## Scientific notation is the behaviour difference, not just the speed

If you read only one part of the formatting section, read this. The library tends to produce more human-readable output than the standard library's `to_string`, which never uses scientific notation, and two examples show the difference:

- ryu: 1.23e40, std: 12300000000000000000000000000000000000000
- ryu: 1.23e-40, std: 0.000000000000000000000000000000000000000123

Both libraries print short decimals such as 0.0000123 without scientific notation. So the difference appears only at the extremes, where one library writes seven characters and the other writes forty.

This is a genuine behavioural difference and it is where the adoption decision actually lives. A formatter that is faster is easy to accept, because the output is the same. A formatter that changes the output shape is a decision, not an optimisation.

The cases that matter are the ones where a downstream consumer parses the text. JSON allows exponent notation, and most JSON parsers accept it. A CSV column read into a spreadsheet, a line protocol that expects a fixed decimal shape, a database column, a log line parsed by a regex with a fixed pattern: these are the places where a change from 1.23e40 to the long form is invisible, and a change the other way is a parsing failure.

The other side of the choice is that the exponent form is what a human would write. A value of 1.23e40 in a log line is legible, and forty zeroes is not. For a tool that prints measurements, diagnostics or a table a person reads, the shorter form is a real usability gain and not merely a size saving.

What the documentation does not claim is worth noting too. It does not say Ryū produces the shortest round trip representation, and the examples show trailing zeros preserved in the short form. If your requirement is a guaranteed round trip, or the shortest string that parses back to the same bits, that is a different property and this crate does not advertise it.

## The small feature trades table size for speed, in one direction only

There is one Cargo feature in the manifest, and the comment describing it is unusually informative for a feature flag.

The `small` feature is described as using smaller lookup tables. Instead of storing every required power of 5, only store every 26th entry, and compute intermediate values with a multiplication. This reduces the lookup table size by about 10x, but only one case and only f64, at the cost of some performance.

So the trade is explicit: roughly a tenth of the table size for a measurable slowdown, and only in the 64-bit case. The 32-bit case is unaffected, which the comment states directly, and it means the feature is for a workload where binary size matters more than the last few nanoseconds.

The rest of the manifest is a good illustration of how a small, careful crate is packaged. The build is pure Rust with one optional dependency, and the documentation is built on docs.rs with custom rustdoc arguments, including `--generate-link-to-definition` and `--generate-macro-expansion` and extern html roots pointing at the core, alloc and std documentation, so the generated docs link into the standard library rather than describing it.

The dev dependencies are for the test and benchmark infrastructure: num_cpus, rand, rand_core and rand_xorshift for generating the random bit patterns, and criterion with `default-features = false` for the benchmark harness.

The `package.metadata.docs.rs` section pins the documentation target to `x86_64-unknown-linux-gnu`, so the published docs are built for one platform.

## When a formatting crate is the wrong dependency

There are three situations where adding Ryū is the wrong move, and the middle one is the most common.

The first is when you need a guaranteed shortest round trip representation. Ryū is fast and its output is readable, but the documentation does not claim the shortest string that parses back to the same floating point bits. If your requirement is that a round trip through text is lossless in the sense of a canonical form, or if you need to interoperate with a system that specifies a particular encoding, you need a different property and this crate does not advertise it.

The second, and this is the common case, is when your project already has a formatter. If you depend on serde_json, or on any crate that formats floats for you, you already have a float to string implementation in your dependency tree, and Ryū is a second one. The question is then whether you want two, because the standard library path and the Ryū path will disagree on the exponent question, and a project that mixes them produces inconsistent output depending on which code path formatted a given number.

The third is when the volume does not justify it. If you format a handful of numbers when a user asks, the difference between 21 nanoseconds and 45 nanoseconds is not something anyone will notice, and you have added a dependency, a licence decision and a second formatting behaviour to maintain.

There is also a licensing point that is easy to miss and easy to get wrong. The crate is licensed under either the Apache License, Version 2.0 or the Boost Software License 1.0 at your option, and the licence field in the manifest reads `Apache-2.0 OR BSL-1.0`. The contribution note says that unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the crate, as defined in the Apache-2.0 licence, shall be dual licensed as above, without any additional terms or conditions. So a contributor is agreeing to two permissive licences rather than picking one, and a downstream user can pick whichever suits their own policy.

## Conclusion

Ryū fits a Rust project that formats a lot of floating point numbers, that wants scientific notation for extreme magnitudes instead of a wall of zeroes, and that is willing to add one dependency. It is the wrong choice if you need the shortest round trip representation, since it does not claim that, and it is largely redundant if your project already depends on serde_json or another formatter, since those pull in their own conversion path. Before adopting, verify two things: that the output format suits your consumers, because a value like 1.23e40 written in exponent form is not what every downstream parser expects, and whether the `small` feature is worth enabling, since it trades roughly ten times smaller lookup tables for some performance and applies only to the f64 case. The code is available under either the Apache License 2.0 or the Boost Software License 1.0 at your option, the last push was on 2026-08-06, and the current version is 1.0.23.

## FAQ

### What is Ryū and where does it come from?

Ryū is a pure Rust implementation of the Ryū algorithm for quickly converting floating point numbers to decimal strings, described as a line-by-line port of Ulf Adams' C implementation. The algorithm comes from the PLDI'18 paper Ryū: fast float-to-string conversion, which includes a complete correctness proof.

### How do I add Ryū to a Rust project?

Add the dependency to Cargo.toml as `ryu = "1.0"` under the `[dependencies]` section. The crate requires Rust 1.71 or newer and has no required dependencies, with an optional `no-panic` dependency for proving the formatting routines cannot unwind.

### How do I format a float with Ryū?

Create a reusable buffer with `ryu::Buffer::new()` and call `buffer.format(value)`, which writes into the buffer and returns a borrowed string. The example in the documentation formats 1.234 and asserts the result is "1.234".

### How does Ryū output differ from the Rust standard library?

Ryū tends to produce more human-readable output, and the standard library's `to_string` never uses scientific notation. The documentation shows ryu printing 1.23e40 and 1.23e-40 where std prints the long zero-padded forms, though both print short decimals such as 0.0000123 without scientific notation.

### What does the small feature in Ryū do?

It uses smaller lookup tables, storing every 26th power of 5 and computing intermediate values with a multiplication, which reduces the lookup table size by about 10x at the cost of some performance, and applies only to the f64 case.

## Sources

- [dtolnay/ryu on GitHub](https://github.com/dtolnay/ryu)
- [Issues](https://github.com/dtolnay/ryu/issues)
- [License: Apache-2.0](https://github.com/dtolnay/ryu/blob/master/LICENSE)
- [README](https://github.com/dtolnay/ryu/blob/master/README.md)
- [Releases](https://github.com/dtolnay/ryu/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dtolnay-ryu
