fast-float2 0.2.4

Fast floating-point number parser.
Documentation

fast-float2

Build Latest Version Documentation Apache 2.0 MIT Rustc 1.37+

This crate provides a super-fast decimal number parser from strings into floats.

[dependencies]
fast-float2 = "0.2.4"

There are no dependencies and the crate can be used in a no_std context by disabling the "std" feature.

Compiler support: rustc 1.37+.

This crate is in maintenance mode for bug fixes (especially security patches): minimal feature enhancements will be accepted. This implementation has been adopted by the Rust standard library: if you do not need parsing directly from bytes and/or partial parsers, you should use FromStr for f32 or f64 instead.

Usage

There's two top-level functions provided: parse() and parse_partial(), both taking either a string or a bytes slice and parsing the input into either f32 or f64:

  • parse() treats the whole string as a decimal number and returns an error if there are invalid characters or if the string is empty.
  • parse_partial() tries to find the longest substring at the beginning of the given input string that can be parsed as a decimal number and, in the case of success, returns the parsed value along the number of characters processed; an error is returned if the string doesn't start with a decimal number or if it is empty. This function is most useful as a building block when constructing more complex parsers, or when parsing streams of data.

Example:

// Parse the entire string as a decimal number.
let s = "1.23e-02";
let x: f32 = fast_float2::parse(s).unwrap();
assert_eq!(x, 0.0123);

// Parse as many characters as possible as a decimal number.
let s = "1.23e-02foo";
let (x, n) = fast_float2::parse_partial::<f32, _>(s).unwrap();
assert_eq!(x, 0.0123);
assert_eq!(n, 8);
assert_eq!(&s[n..], "foo");

Details

This crate is a direct port of Daniel Lemire's fast_float C++ library (valuable discussions with Daniel while porting it helped shape the crate and get it to the performance level it's at now), with some Rust-specific tweaks. Please see the original repository for many useful details regarding the algorithm and the implementation.

The parser is locale-independent. The resulting value is the closest floating-point values (using either f32 or f64), using the "round to even" convention for values that would otherwise fall right in-between two values. That is, we provide exact parsing according to the IEEE standard.

Infinity and NaN values can be parsed, along with scientific notation.

Both little-endian and big-endian platforms are equally supported, with extra optimizations enabled on little-endian architectures.

Since fast-float-rust is unmaintained, this is a fork containing the patches and security updates.

Features

no-panic: when this feature is enabled, the crate guarantees that it will not trigger a runtime panic. This disables runtime index bounds checking and therefore introduces 1 non-local safety invariant. See decimal.parse_decimal for the invariant conditions. Note that no-panic does not work on Rust versions before 1.60.0.

Testing

There are a few ways this crate is tested:

  • A suite of explicit tests (taken from the original library) covering lots of edge cases.
  • A file-based test suite (taken from the original library; credits to Nigel Tao), ~5M tests.
  • All 4B float32 numbers are exhaustively roundtripped via ryu formatter.
  • Roundtripping a large quantity of random float64 numbers via ryu formatter.
  • Roundtripping float64 numbers and fuzzing random input strings via cargo-fuzz.
  • All explicit test suites run on CI; roundtripping and fuzzing are run manually.

Performance

The presented parser seems to beat all of the existing C/C++/Rust float parsers known to us at the moment by a large margin, in all of the datasets we tested it on so far – see detailed benchmarks below (the only exception being the original fast_float C++ library, of course – performance of which is within noise bounds of this crate). On modern machines like Apple M1, parsing throughput can reach up to 1.5 GB/s.

While various details regarding the algorithm can be found in the repository for the original C++ library, here are few brief notes:

  • The parser is specialized to work lightning-fast on inputs with at most 19 significant digits (which constitutes the so called "fast-path"). We believe that most real-life inputs should fall under this category, and we treat longer inputs as "degenerate" edge cases since it inevitable causes overflows and loss of precision.
  • If the significand happens to be longer than 19 digits, the parser falls back to the "slow path", in which case its performance roughly matches that of the top Rust/C++ libraries (and still beats them most of the time, although not by a lot).
  • On little-endian systems, there's additional optimizations for numbers with more than 8 digits after the decimal point.

Benchmarks

Below are tables of best timings in nanoseconds for parsing a single number into a 64-bit float (using the median score, lower is better).

Intel i7-14700K

  • CPU: Intel i7-14700K 3.40GHz
  • OS: Ubuntu 24.04 (WSL2)
  • Rust: 1.81
  • C++: GCC 13.2.0
canada mesh uniform bi iei rec32
fast-float2 9.98 5.56 10.08 56.19 14.52 15.09
fast-float 9.77 5.04 9.05 57.52 14.40 14.23
lexical 10.62 4.93 9.92 26.40 12.43 14.40
from_str 11.59 5.92 11.23 35.92 14.75 16.76
fast_float (C++) 12.58 6.35 11.86 31.55 12.22 11.97
abseil (C++) 25.32 15.70 25.88 43.42 23.54 26.75
netlib (C) 35.10 10.22 37.72 68.63 23.07 38.23
strtod (C) 52.63 26.47 46.51 88.11 33.37 53.36
doubleconversion (C++) 32.50 14.69 47.80 70.01 205.72 45.66

AMD EPYC 7763 64-Core Processor (Linux)

  • CPU: AMD EPYC 7763 64-Core Processor 3.20GHz
  • OS: Ubuntu 24.04.1
  • Rust: 1.83
  • C++: GCC 13.2.0
  • Environment: Github Actions (2.321.0)
canada mesh uniform bi iei rec32
fast-float2 19.83 10.42 18.64 80.03 26.12 27.70
fast-float 19.17 9.89 17.34 82.37 25.26 27.22
lexical 18.89 8.41 16.83 47.66 22.08 26.99
from_str 22.90 12.72 22.10 62.20 27.51 30.80
fast_float (C++) 20.71 10.72 24.63 82.85 24.24 19.60
abseil (C++) 31.03 23.78 32.82 76.05 28.41 35.03
netlib (C) 54.22 20.12 68.68 82.64 32.81 69.43
strtod (C) 100.10 52.08 85.32 192.31 75.08 97.85
doubleconversion (C++) 75.13 31.98 87.64 170.06 124.69 87.26

AMD EPYC 7763 64-Core Processor (Windows)

  • CPU: AMD EPYC 7763 64-Core Processor 3.20GHz
  • OS: Windows Server 2022 (10.0.20348)
  • Rust: 1.83
  • C++: MSVC 19.42.34435.0
  • Environment: Github Actions (2.321.0)
canada mesh uniform bi iei rec32
fast-float2 20.92 10.02 19.34 94.37 27.09 30.84
fast-float 19.72 9.65 18.46 86.85 25.75 30.05
lexical 19.15 8.80 17.92 51.14 22.16 28.34
from_str 25.93 13.49 23.36 78.82 27.80 35.58
fast_float (C++) 64.89 47.46 64.40 104.36 55.44 69.29
abseil (C++) 37.77 33.10 41.24 136.86 37.11 47.32
netlib (C) 53.76 28.78 60.96 76.35 44.33 62.96
strtod (C) 181.47 85.95 192.35 262.81 125.37 204.94
doubleconversion (C++) 119.02 28.78 128.16 232.35 110.97 129.63

Apple M1 (macOS)

  • CPU: AMD EPYC 7763 64-Core Processor 3.20GHz
  • OS: macOS (14.7.1)
  • Rust: 1.83
  • C++: Clang (Apple) 15.0.0.15000309
  • Environment: Github Actions
canada mesh uniform bi iei rec32
fast-float2 15.47 6.54 11.62 94.35 20.55 17.78
fast-float 14.56 6.40 11.09 92.89 21.19 17.06
lexical 14.13 6.55 11.96 35.99 15.93 18.91
from_str 17.67 7.93 13.88 58.60 19.68 19.92
fast_float (C++) 17.42 10.40 15.14 87.33 21.82 14.53
abseil (C++) 20.94 17.31 22.50 63.86 24.69 25.19
netlib (C) 45.05 13.79 52.38 156.25 36.10 51.36
strtod (C) 25.88 14.25 27.08 85.32 23.03 26.86
doubleconversion (C++) 53.39 21.50 73.15 120.63 52.88 70.47

Note that the random number generation seems to differ between C/C++ and Rust, since the Rust implementations are slightly faster for pre-determined datasets like canada and mesh, but equivalent random number generators are slightly slower. Any performance penalty with fast-float2 occurred due to fixing the UB in check_len. The massive performance differences between fast-float (Rust) and fast_float (C++) are expected due to a faster fallback algorithms (#96 and #104) used in these cases.

Parsers

  • fast-float2 - this very crate
  • fast-float - the pre-ported variant
  • lexicallexical_core, v1.0.05
  • from_str – Rust standard library, FromStr trait
  • fast_float (C++) – original C++ implementation of 'fast-float' method
  • abseil (C++) – Abseil C++ Common Libraries
  • netlib (C++) – C++ Network Library
  • strtod (C) – C standard library

Datasets

  • canada – numbers in canada.txt file
  • mesh – numbers in mesh.txt file
  • uniform – uniform random numbers from 0 to 1
  • bi – large, integer-only floats <!-- big_ints -- >
  • int_e_int – random numbers of format %de%d
  • rec32 – reciprocals of random 32-bit integers

Notes

  • The two test files referred above can be found in this repository.
  • The Rust part of the table (along with a few other benchmarks) can be generated via the benchmark tool that can be found under extras/simple-bench of this repo.
  • The C/C++ part of the table (along with a few other benchmarks and parsers) can be generated via a C++ utility that can be found in this repository.

References

License