From 4865258ae79cadee2068f7f341f1d9c4e767715f Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Sat, 14 Sep 2024 10:09:52 +0800 Subject: [PATCH 1/5] Improve performance of ParseUfloat function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced `offset` handling logic with more efficient math.Pow10 based calculation. goos: linux goarch: amd64 pkg: github.com/valyala/fasthttp cpu: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ ParseUfloat-8 44.22n ± 0% 31.06n ± 0% -29.76% (n=50) --- bytesconv.go | 8 ++++---- bytesconv_timing_test.go | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/bytesconv.go b/bytesconv.go index f01a6c11e8..1f0784e1b2 100644 --- a/bytesconv.go +++ b/bytesconv.go @@ -187,7 +187,7 @@ func ParseUfloat(buf []byte) (float64, error) { } b := buf var v uint64 - offset := 1.0 + offset := 0 var pointFound bool for i, c := range b { if c < '0' || c > '9' { @@ -217,16 +217,16 @@ func ParseUfloat(buf []byte) (float64, error) { if err != nil { return -1, errInvalidFloatExponent } - return float64(v) * offset * math.Pow10(minus*vv), nil + return float64(v) * math.Pow10(minus*vv+offset), nil } return -1, errUnexpectedFloatChar } v = 10*v + uint64(c-'0') if pointFound { - offset /= 10 + offset-- } } - return float64(v) * offset, nil + return float64(v) * math.Pow10(offset), nil } var ( diff --git a/bytesconv_timing_test.go b/bytesconv_timing_test.go index 334376a94d..1c1aa4fc0d 100644 --- a/bytesconv_timing_test.go +++ b/bytesconv_timing_test.go @@ -163,3 +163,24 @@ func BenchmarkAppendUnquotedArgSlowPath(b *testing.B) { } }) } + +func BenchmarkParseUfloat(b *testing.B) { + src := [][]byte{ + []byte("0"), + []byte("1234566789."), + []byte(".1234556778"), + []byte("123.456"), + []byte("123456789"), + []byte("1234e23"), + []byte("1234E-51"), + []byte("1.234e+32"), + []byte("123456789123456789.987654321"), + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + for i := range src { + ParseUfloat(src[i]) + } + } + }) +} From 7e44b13def1a5c85427d2adc0c7a49055a3aa87a Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Sat, 14 Sep 2024 10:26:52 +0800 Subject: [PATCH 2/5] fix: lint error return value is not checked --- bytesconv_timing_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bytesconv_timing_test.go b/bytesconv_timing_test.go index 1c1aa4fc0d..e9626c54a8 100644 --- a/bytesconv_timing_test.go +++ b/bytesconv_timing_test.go @@ -179,7 +179,10 @@ func BenchmarkParseUfloat(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { for i := range src { - ParseUfloat(src[i]) + _, err := ParseUfloat(src[i]) + if err != nil { + b.Fatalf("unexpected error: %v", err) + } } } }) From 6fe140ccc29ed9526ed6acf9c72db28d678f8f5b Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Sun, 22 Sep 2024 14:40:51 +0800 Subject: [PATCH 3/5] Handling uint64 overflow issues --- bytesconv.go | 11 ++++++++--- bytesconv_test.go | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bytesconv.go b/bytesconv.go index 1f0784e1b2..1deb6220fe 100644 --- a/bytesconv.go +++ b/bytesconv.go @@ -221,9 +221,14 @@ func ParseUfloat(buf []byte) (float64, error) { } return -1, errUnexpectedFloatChar } - v = 10*v + uint64(c-'0') - if pointFound { - offset-- + // v is limited by the uint64 upper bound, and can safely handle up to 19 digits at most. + if i < 19 { + v = 10*v + uint64(c-'0') + if pointFound { + offset-- + } + } else if !pointFound { + offset++ } } return float64(v) * math.Pow10(offset), nil diff --git a/bytesconv_test.go b/bytesconv_test.go index d5ca5e4341..c56ca4e938 100644 --- a/bytesconv_test.go +++ b/bytesconv_test.go @@ -234,6 +234,11 @@ func TestParseUfloatSuccess(t *testing.T) { testParseUfloatSuccess(t, "1234e2", 1234e2) testParseUfloatSuccess(t, "1234E-5", 1234e-5) testParseUfloatSuccess(t, "1.234e+3", 1.234e+3) + testParseUfloatSuccess(t, "1234e23", 1234e23) + testParseUfloatSuccess(t, "1.234e+32", 1.234e+32) + testParseUfloatSuccess(t, "123456789123456789.987654321", 123456789123456789.987654321) + testParseUfloatSuccess(t, "1.23456789123456789987654321", 1.23456789123456789987654321) + testParseUfloatSuccess(t, "340282346638528859811704183484516925440", 340282346638528859811704183484516925440) } func TestParseUfloatError(t *testing.T) { From 58c2ed85a0cc7a133cfa582f00fd60e684d9d80f Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:29:36 +0800 Subject: [PATCH 4/5] Implement ParseUfloat by calling strconv.ParseFloat --- bytesconv.go | 66 +++++++---------------------------------------- bytesconv_test.go | 5 ++++ 2 files changed, 14 insertions(+), 57 deletions(-) diff --git a/bytesconv.go b/bytesconv.go index 1deb6220fe..053da6a700 100644 --- a/bytesconv.go +++ b/bytesconv.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "math" "net" "strconv" "sync" @@ -172,66 +171,19 @@ func parseUintBuf(b []byte) (int, int, error) { return v, n, nil } -var ( - errEmptyFloat = errors.New("empty float number") - errDuplicateFloatPoint = errors.New("duplicate point found in float number") - errUnexpectedFloatEnd = errors.New("unexpected end of float number") - errInvalidFloatExponent = errors.New("invalid float number exponent") - errUnexpectedFloatChar = errors.New("unexpected char found in float number") -) - // ParseUfloat parses unsigned float from buf. func ParseUfloat(buf []byte) (float64, error) { - if len(buf) == 0 { - return -1, errEmptyFloat + // The implementation of parsing a float string is not easy. + // We believe that the conservative approach is to call strconv.ParseFloat. + // https://github.com/valyala/fasthttp/pull/1865 + res, err := strconv.ParseFloat(b2s(buf), 64) + if res < 0 { + return -1, errors.New("negative input is invalid") } - b := buf - var v uint64 - offset := 0 - var pointFound bool - for i, c := range b { - if c < '0' || c > '9' { - if c == '.' { - if pointFound { - return -1, errDuplicateFloatPoint - } - pointFound = true - continue - } - if c == 'e' || c == 'E' { - if i+1 >= len(b) { - return -1, errUnexpectedFloatEnd - } - b = b[i+1:] - minus := -1 - switch b[0] { - case '+': - b = b[1:] - minus = 1 - case '-': - b = b[1:] - default: - minus = 1 - } - vv, err := ParseUint(b) - if err != nil { - return -1, errInvalidFloatExponent - } - return float64(v) * math.Pow10(minus*vv+offset), nil - } - return -1, errUnexpectedFloatChar - } - // v is limited by the uint64 upper bound, and can safely handle up to 19 digits at most. - if i < 19 { - v = 10*v + uint64(c-'0') - if pointFound { - offset-- - } - } else if !pointFound { - offset++ - } + if err != nil { + return -1, err } - return float64(v) * math.Pow10(offset), nil + return res, err } var ( diff --git a/bytesconv_test.go b/bytesconv_test.go index c56ca4e938..655f8431b0 100644 --- a/bytesconv_test.go +++ b/bytesconv_test.go @@ -239,6 +239,7 @@ func TestParseUfloatSuccess(t *testing.T) { testParseUfloatSuccess(t, "123456789123456789.987654321", 123456789123456789.987654321) testParseUfloatSuccess(t, "1.23456789123456789987654321", 1.23456789123456789987654321) testParseUfloatSuccess(t, "340282346638528859811704183484516925440", 340282346638528859811704183484516925440) + testParseUfloatSuccess(t, "00000000000000000001", 00000000000000000001) } func TestParseUfloatError(t *testing.T) { @@ -268,6 +269,10 @@ func TestParseUfloatError(t *testing.T) { // missing exponent testParseUfloatError(t, "123534e") + + // negative number + testParseUfloatError(t, "-1") + testParseUfloatError(t, "-Inf") } func testParseUfloatError(t *testing.T, s string) { From bc0a3dcef46e6bcca0371a9d9e26f493457d652c Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:37:57 +0800 Subject: [PATCH 5/5] fix: lint error --- bytesconv_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bytesconv_test.go b/bytesconv_test.go index 655f8431b0..b9a2dbfb08 100644 --- a/bytesconv_test.go +++ b/bytesconv_test.go @@ -239,7 +239,7 @@ func TestParseUfloatSuccess(t *testing.T) { testParseUfloatSuccess(t, "123456789123456789.987654321", 123456789123456789.987654321) testParseUfloatSuccess(t, "1.23456789123456789987654321", 1.23456789123456789987654321) testParseUfloatSuccess(t, "340282346638528859811704183484516925440", 340282346638528859811704183484516925440) - testParseUfloatSuccess(t, "00000000000000000001", 00000000000000000001) + testParseUfloatSuccess(t, "00000000000000000001", 1) } func TestParseUfloatError(t *testing.T) {