Skip to content

Commit f502bd3

Browse files
committed
Auto merge of #86761 - Alexhuszagh:master, r=estebank
Update Rust Float-Parsing Algorithms to use the Eisel-Lemire algorithm. # Summary Rust, although it implements a correct float parser, has major performance issues in float parsing. Even for common floats, the performance can be 3-10x [slower](https://arxiv.org/pdf/2101.11408.pdf) than external libraries such as [lexical](https://github.com/Alexhuszagh/rust-lexical) and [fast-float-rust](https://github.com/aldanor/fast-float-rust). Recently, major advances in float-parsing algorithms have been developed by Daniel Lemire, along with others, and implement a fast, performant, and correct float parser, with speeds up to 1200 MiB/s on Apple's M1 architecture for the [canada](https://github.com/lemire/simple_fastfloat_benchmark/blob/0e2b5d163d4074cc0bde2acdaae78546d6e5c5f1/data/canada.txt) dataset, 10x faster than Rust's 130 MiB/s. In addition, [edge-cases](#85234) in Rust's [dec2flt](https://github.com/rust-lang/rust/tree/868c702d0c9a471a28fb55f0148eb1e3e8b1dcc5/library/core/src/num/dec2flt) algorithm can lead to over a 1600x slowdown relative to efficient algorithms. This is due to the use of Clinger's correct, but slow [AlgorithmM and Bellepheron](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.45.4152&rep=rep1&type=pdf), which have been improved by faster big-integer algorithms and the Eisel-Lemire algorithm, respectively. Finally, this algorithm provides substantial improvements in the number of floats the Rust core library can parse. Denormal floats with a large number of digits cannot be parsed, due to use of the `Big32x40`, which simply does not have enough digits to round a float correctly. Using a custom decimal class, with much simpler logic, we can parse all valid decimal strings of any digit count. ```rust // Issue in Rust's dec2fly. "2.47032822920623272088284396434110686182e-324".parse::<f64>(); // Err(ParseFloatError { kind: Invalid }) ``` # Solution This pull request implements the Eisel-Lemire algorithm, modified from [fast-float-rust](https://github.com/aldanor/fast-float-rust) (which is licensed under Apache 2.0/MIT), along with numerous modifications to make it more amenable to inclusion in the Rust core library. The following describes both features in fast-float-rust and improvements in fast-float-rust for inclusion in core. **Documentation** Extensive documentation has been added to ensure the code base may be maintained by others, which explains the algorithms as well as various associated constants and routines. For example, two seemingly magical constants include documentation to describe how they were derived as follows: ```rust // Round-to-even only happens for negative values of q // when q ≥ −4 in the 64-bit case and when q ≥ −17 in // the 32-bitcase. // // When q ≥ 0,we have that 5^q ≤ 2m+1. In the 64-bit case,we // have 5^q ≤ 2m+1 ≤ 2^54 or q ≤ 23. In the 32-bit case,we have // 5^q ≤ 2m+1 ≤ 2^25 or q ≤ 10. // // When q < 0, we have w ≥ (2m+1)×5^−q. We must have that w < 2^64 // so (2m+1)×5^−q < 2^64. We have that 2m+1 > 2^53 (64-bit case) // or 2m+1 > 2^24 (32-bit case). Hence,we must have 2^53×5^−q < 2^64 // (64-bit) and 2^24×5^−q < 2^64 (32-bit). Hence we have 5^−q < 2^11 // or q ≥ −4 (64-bit case) and 5^−q < 2^40 or q ≥ −17 (32-bitcase). // // Thus we have that we only need to round ties to even when // we have that q ∈ [−4,23](in the 64-bit case) or q∈[−17,10] // (in the 32-bit case). In both cases,the power of five(5^|q|) // fits in a 64-bit word. const MIN_EXPONENT_ROUND_TO_EVEN: i32; const MAX_EXPONENT_ROUND_TO_EVEN: i32; ``` This ensures maintainability of the code base. **Improvements for Disguised Fast-Path Cases** The fast path in float parsing algorithms attempts to use native, machine floats to represent both the significant digits and the exponent, which is only possible if both can be exactly represented without rounding. In practice, this means that the significant digits must be 53-bits or less and the then exponent must be in the range `[-22, 22]` (for an f64). This is similar to the existing dec2flt implementation. However, disguised fast-path cases exist, where there are few significant digits and an exponent above the valid range, such as `1.23e25`. In this case, powers-of-10 may be shifted from the exponent to the significant digits, discussed at length in #85198. **Digit Parsing Improvements** Typically, integers are parsed from string 1-at-a-time, requiring unnecessary multiplications which can slow down parsing. An approach to parse 8 digits at a time using only 3 multiplications is described in length [here](https://johnnylee-sde.github.io/Fast-numeric-string-to-int/). This leads to significant performance improvements, and is implemented for both big and little-endian systems. **Unsafe Changes** Relative to fast-float-rust, this library makes less use of unsafe functionality and clearly documents it. This includes the refactoring and documentation of numerous unsafe methods undesirably marked as safe. The original code would look something like this, which is deceptively marked as safe for unsafe functionality. ```rust impl AsciiStr { #[inline] pub fn step_by(&mut self, n: usize) -> &mut Self { unsafe { self.ptr = self.ptr.add(n) }; self } } ... #[inline] fn parse_scientific(s: &mut AsciiStr<'_>) -> i64 { // the first character is 'e'/'E' and scientific mode is enabled let start = *s; s.step(); ... } ``` The new code clearly documents safety concerns, and does not mark unsafe functionality as safe, leading to better safety guarantees. ```rust impl AsciiStr { /// Advance the view by n, advancing it in-place to (n..). pub unsafe fn step_by(&mut self, n: usize) -> &mut Self { // SAFETY: same as step_by, safe as long n is less than the buffer length self.ptr = unsafe { self.ptr.add(n) }; self } } ... /// Parse the scientific notation component of a float. fn parse_scientific(s: &mut AsciiStr<'_>) -> i64 { let start = *s; // SAFETY: the first character is 'e'/'E' and scientific mode is enabled unsafe { s.step(); } ... } ``` This allows us to trivially demonstrate the new implementation of dec2flt is safe. **Inline Annotations Have Been Removed** In the previous implementation of dec2flt, inline annotations exist practically nowhere in the entire module. Therefore, these annotations have been removed, which mostly does not impact [performance](aldanor/fast-float-rust#15 (comment)). **Fixed Correctness Tests** Numerous compile errors in `src/etc/test-float-parse` were present, due to deprecation of `time.clock()`, as well as the crate dependencies with `rand`. The tests have therefore been reworked as a [crate](https://github.com/Alexhuszagh/rust/tree/master/src/etc/test-float-parse), and any errors in `runtests.py` have been patched. **Undefined Behavior** An implementation of `check_len` which relied on undefined behavior (in fast-float-rust) has been refactored, to ensure that the behavior is well-defined. The original code is as follows: ```rust #[inline] pub fn check_len(&self, n: usize) -> bool { unsafe { self.ptr.add(n) <= self.end } } ``` And the new implementation is as follows: ```rust /// Check if the slice at least `n` length. fn check_len(&self, n: usize) -> bool { n <= self.as_ref().len() } ``` Note that this has since been fixed in [fast-float-rust](aldanor/fast-float-rust#29). **Inferring Binary Exponents** Rather than explicitly store binary exponents, this new implementation infers them from the decimal exponent, reducing the amount of static storage required. This removes the requirement to store [611 i16s](https://github.com/rust-lang/rust/blob/868c702d0c9a471a28fb55f0148eb1e3e8b1dcc5/library/core/src/num/dec2flt/table.rs#L8). # Code Size The code size, for all optimizations, does not considerably change relative to before for stripped builds, however it is **significantly** smaller prior to stripping the resulting binaries. These binary sizes were calculated on x86_64-unknown-linux-gnu. **new** Using rustc version 1.55.0-dev. opt-level|size|size(stripped) |:-:|:-:|:-:| 0|400k|300K 1|396k|292K 2|392k|292K 3|392k|296K s|396k|292K z|396k|292K **old** Using rustc version 1.53.0-nightly. opt-level|size|size(stripped) |:-:|:-:|:-:| 0|3.2M|304K 1|3.2M|292K 2|3.1M|284K 3|3.1M|284K s|3.1M|284K z|3.1M|284K # Correctness The dec2flt implementation passes all of Rust's unittests and comprehensive float parsing tests, along with numerous other tests such as Nigel Toa's comprehensive float [tests](https://github.com/nigeltao/parse-number-fxx-test-data) and Hrvoje Abraham [strtod_tests](https://github.com/ahrvoje/numerics/blob/master/strtod/strtod_tests.toml). Therefore, it is unlikely that this algorithm will incorrectly round parsed floats. # Issues Addressed This will fix and close the following issues: - resolves #85198 - resolves #85214 - resolves #85234 - fixes #31407 - fixes #31109 - fixes #53015 - resolves #68396 - closes aldanor/fast-float-rust#15
2 parents 64d171b + 8752b40 commit f502bd3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2530
-2823
lines changed

compiler/rustc_middle/src/mir/interpret/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ pub enum LitToConstError {
172172
/// This is used for graceful error handling (`delay_span_bug`) in
173173
/// type checking (`Const::from_anon_const`).
174174
TypeError,
175-
UnparseableFloat,
176175
Reported,
177176
}
178177

compiler/rustc_mir_build/src/thir/constant.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ crate fn lit_to_const<'tcx>(
4646
(ast::LitKind::Int(n, _), ty::Uint(_)) | (ast::LitKind::Int(n, _), ty::Int(_)) => {
4747
trunc(if neg { (*n as i128).overflowing_neg().0 as u128 } else { *n })?
4848
}
49-
(ast::LitKind::Float(n, _), ty::Float(fty)) => {
50-
parse_float(*n, *fty, neg).map_err(|_| LitToConstError::UnparseableFloat)?
51-
}
49+
(ast::LitKind::Float(n, _), ty::Float(fty)) => parse_float(*n, *fty, neg),
5250
(ast::LitKind::Bool(b), ty::Bool) => ConstValue::Scalar(Scalar::from_bool(*b)),
5351
(ast::LitKind::Char(c), ty::Char) => ConstValue::Scalar(Scalar::from_char(*c)),
5452
(ast::LitKind::Err(_), _) => return Err(LitToConstError::Reported),
@@ -57,12 +55,14 @@ crate fn lit_to_const<'tcx>(
5755
Ok(ty::Const::from_value(tcx, lit, ty))
5856
}
5957

60-
fn parse_float<'tcx>(num: Symbol, fty: ty::FloatTy, neg: bool) -> Result<ConstValue<'tcx>, ()> {
58+
fn parse_float<'tcx>(num: Symbol, fty: ty::FloatTy, neg: bool) -> ConstValue<'tcx> {
6159
let num = num.as_str();
6260
use rustc_apfloat::ieee::{Double, Single};
6361
let scalar = match fty {
6462
ty::FloatTy::F32 => {
65-
let rust_f = num.parse::<f32>().map_err(|_| ())?;
63+
let rust_f = num
64+
.parse::<f32>()
65+
.unwrap_or_else(|e| panic!("f32 failed to parse `{}`: {:?}", num, e));
6666
let mut f = num.parse::<Single>().unwrap_or_else(|e| {
6767
panic!("apfloat::ieee::Single failed to parse `{}`: {:?}", num, e)
6868
});
@@ -82,7 +82,9 @@ fn parse_float<'tcx>(num: Symbol, fty: ty::FloatTy, neg: bool) -> Result<ConstVa
8282
Scalar::from_f32(f)
8383
}
8484
ty::FloatTy::F64 => {
85-
let rust_f = num.parse::<f64>().map_err(|_| ())?;
85+
let rust_f = num
86+
.parse::<f64>()
87+
.unwrap_or_else(|e| panic!("f64 failed to parse `{}`: {:?}", num, e));
8688
let mut f = num.parse::<Double>().unwrap_or_else(|e| {
8789
panic!("apfloat::ieee::Double failed to parse `{}`: {:?}", num, e)
8890
});
@@ -103,5 +105,5 @@ fn parse_float<'tcx>(num: Symbol, fty: ty::FloatTy, neg: bool) -> Result<ConstVa
103105
}
104106
};
105107

106-
Ok(ConstValue::Scalar(scalar))
108+
ConstValue::Scalar(scalar)
107109
}

compiler/rustc_mir_build/src/thir/cx/mod.rs

-6
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,6 @@ impl<'tcx> Cx<'tcx> {
6767

6868
match self.tcx.at(sp).lit_to_const(LitToConstInput { lit, ty, neg }) {
6969
Ok(c) => c,
70-
Err(LitToConstError::UnparseableFloat) => {
71-
// FIXME(#31407) this is only necessary because float parsing is buggy
72-
self.tcx.sess.span_err(sp, "could not evaluate float literal (see issue #31407)");
73-
// create a dummy value and continue compiling
74-
self.tcx.const_error(ty)
75-
}
7670
Err(LitToConstError::Reported) => {
7771
// create a dummy value and continue compiling
7872
self.tcx.const_error(ty)

compiler/rustc_mir_build/src/thir/pattern/check_match.rs

+2-10
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl<'tcx> Visitor<'tcx> for MatchVisitor<'_, 'tcx> {
8484
}
8585

8686
impl PatCtxt<'_, '_> {
87-
fn report_inlining_errors(&self, pat_span: Span) {
87+
fn report_inlining_errors(&self) {
8888
for error in &self.errors {
8989
match *error {
9090
PatternError::StaticInPattern(span) => {
@@ -96,14 +96,6 @@ impl PatCtxt<'_, '_> {
9696
PatternError::ConstParamInPattern(span) => {
9797
self.span_e0158(span, "const parameters cannot be referenced in patterns")
9898
}
99-
PatternError::FloatBug => {
100-
// FIXME(#31407) this is only necessary because float parsing is buggy
101-
rustc_middle::mir::interpret::struct_error(
102-
self.tcx.at(pat_span),
103-
"could not evaluate float literal (see issue #31407)",
104-
)
105-
.emit();
106-
}
10799
PatternError::NonConstPath(span) => {
108100
rustc_middle::mir::interpret::struct_error(
109101
self.tcx.at(span),
@@ -142,7 +134,7 @@ impl<'tcx> MatchVisitor<'_, 'tcx> {
142134
let pattern: &_ = cx.pattern_arena.alloc(expand_pattern(pattern));
143135
if !patcx.errors.is_empty() {
144136
*have_errors = true;
145-
patcx.report_inlining_errors(pat.span);
137+
patcx.report_inlining_errors();
146138
}
147139
(pattern, pattern_ty)
148140
}

compiler/rustc_mir_build/src/thir/pattern/mod.rs

-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ crate enum PatternError {
3131
AssocConstInPattern(Span),
3232
ConstParamInPattern(Span),
3333
StaticInPattern(Span),
34-
FloatBug,
3534
NonConstPath(Span),
3635
}
3736

@@ -563,10 +562,6 @@ impl<'a, 'tcx> PatCtxt<'a, 'tcx> {
563562
LitToConstInput { lit: &lit.node, ty: self.typeck_results.expr_ty(expr), neg };
564563
match self.tcx.at(expr.span).lit_to_const(lit_input) {
565564
Ok(val) => *self.const_to_pat(val, expr.hir_id, lit.span, false).kind,
566-
Err(LitToConstError::UnparseableFloat) => {
567-
self.errors.push(PatternError::FloatBug);
568-
PatKind::Wild
569-
}
570565
Err(LitToConstError::Reported) => PatKind::Wild,
571566
Err(LitToConstError::TypeError) => bug!("lower_lit: had type error"),
572567
}

0 commit comments

Comments
 (0)