Skip to content

Performance cliff when parsing f64 above 1e305 #53015

@dtolnay

Description

@dtolnay
Member

I noticed the following performance cliff of 3 orders of magnitude:

#![feature(test)]

#[cfg(test)]
extern crate test;

#[bench]
fn bench_parse_1e304(b: &mut test::Bencher) {
    // Anything under 1e305 is reasonably fast: ~120 ns/iter
    b.iter(|| "1.234e304".parse::<f64>());
}

#[bench]
fn bench_parse_1e305(b: &mut test::Bencher) {
    // Anything 1e305 or above is slower by 3 orders of magnitude: ~220,000 ns/iter 
    b.iter(|| "1.234e305".parse::<f64>());
}

Playground that reproduces the same behavior.

I tried Go and it parses both inputs in 60 ns. The resulting bits are the same in both implementations.

package test

import (
	"strconv"
	"testing"
)

func BenchmarkFast(b *testing.B) {
	for n := 0; n < b.N; n++ {
		strconv.ParseFloat("1.234e304", 64)
	}
}

func BenchmarkAlsoFast(b *testing.B) {
	for n := 0; n < b.N; n++ {
		strconv.ParseFloat("1.234e305", 64)
	}
}

Activity

added
I-slowIssue: Problems and improvements with respect to performance of generated code.
T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.
C-bugCategory: This is a bug.
on Aug 3, 2018
kennytm

kennytm commented on Aug 3, 2018

@kennytm
Member

Parsing 0.99e305 is also fast 🤔.

use std::time::SystemTime;

fn bench(input: &str) {
    let begin = SystemTime::now();
    for _ in 0..2000 {
        input.parse::<f64>().unwrap();
    }
    let elapsed = begin.elapsed().unwrap() / 2000;
    println!("{}: {:?}", input, elapsed);
}

fn main() {
    bench("9.9e305"); // ~300000ns
    bench("0.99e305"); // ~300ns
    bench("9.9e304"); // ~300ns
    
    bench("1.0e-288"); // ~200000ns
    bench("10.0e-288"); // ~306ns
    bench("1.0e-287"); // ~300ns
}
dtolnay

dtolnay commented on Aug 3, 2018

@dtolnay
MemberAuthor

Ah, I found this note in #27307:

Short numbers that hit the fast paths (f64 multiplication or shortcuts to zero/inf) have performance in the same order of magnitude as the old code tens of nanoseconds. Numbers that are delegated to Algorithm Bellerophon (using floats with 64 bit significand, implemented in software) are slower, but not drastically so (couple hundred nanoseconds).

Numbers that need the AlgorithmM fallback (for f64, roughly everything below 1e-305 and above 1e305) take far, far longer, hundreds of microseconds.

Seems like we may want to look at what Go is doing to handle this better.

Mentioning @rkruppe.

kennytm

kennytm commented on Aug 3, 2018

@kennytm
Member

https://golang.org/src/strconv/atof.go:

// decimal to binary floating point conversion.
// Algorithm:
//   1) Store input in multiprecision decimal.
//   2) Multiply/divide decimal by powers of two until in range [0.5, 1)
//   3) Multiply by 2^precision and round to get mantissa.
hanna-kruppe

hanna-kruppe commented on Aug 3, 2018

@hanna-kruppe
Contributor

The general shape of the algorithm as quoted by @kennytm is also what we're doing in the slow path (edit: I misread but it also involved bignums so in any case Go's algorithm doesn't seem like it would fit in 60ns). I haven't read the Go code in detail yet, but it also seems to have fast paths that don't construct any bignums. So my assumption is that this fast path is taken more often than the one in the Rust implementation (i.e., the check whether it's safe is more accurate) and that's why 1e305 is faster. Presumably there will be a similar performance cliff if you find an input that does force Go to actually go the bignum route.

8 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.I-slowIssue: Problems and improvements with respect to performance of generated code.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @kennytm@dtolnay@hanna-kruppe

      Issue actions

        Performance cliff when parsing f64 above 1e305 · Issue #53015 · rust-lang/rust