From 46aa9f5437b000fad3696b0cd9fd97995da16411 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Jun 2017 19:16:19 +0300 Subject: [PATCH] strconv: optimize Atoi for common case MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Benchmark results on GOOS=linux: GOARCH=amd64 name old time/op new time/op delta Atoi/Pos/7bit-4 20.1ns ± 2% 8.6ns ± 1% -57.34% (p=0.000 n=10+10) Atoi/Pos/26bit-4 25.8ns ± 7% 11.9ns ± 0% -53.91% (p=0.000 n=10+8) Atoi/Pos/31bit-4 27.3ns ± 2% 13.2ns ± 1% -51.56% (p=0.000 n=10+10) Atoi/Pos/56bit-4 37.2ns ± 5% 18.2ns ± 1% -51.26% (p=0.000 n=10+10) Atoi/Pos/63bit-4 38.7ns ± 1% 38.6ns ± 1% ~ (p=0.297 n=9+10) Atoi/Neg/7bit-4 17.6ns ± 1% 7.2ns ± 0% -59.22% (p=0.000 n=10+10) Atoi/Neg/26bit-4 24.4ns ± 1% 12.4ns ± 1% -49.28% (p=0.000 n=10+10) Atoi/Neg/31bit-4 26.9ns ± 0% 14.0ns ± 1% -47.88% (p=0.000 n=7+10) Atoi/Neg/56bit-4 36.2ns ± 1% 19.5ns ± 0% -46.24% (p=0.000 n=10+9) Atoi/Neg/63bit-4 38.9ns ± 1% 38.8ns ± 1% ~ (p=0.385 n=9+10) GOARCH=386 name old time/op new time/op delta Atoi/Pos/7bit-4 89.6ns ± 1% 8.2ns ± 1% -90.84% (p=0.000 n=9+10) Atoi/Pos/26bit-4 187ns ± 2% 12ns ± 1% -93.71% (p=0.000 n=10+9) Atoi/Pos/31bit-4 225ns ± 1% 225ns ± 1% ~ (p=0.995 n=10+10) Atoi/Neg/7bit-4 86.2ns ± 1% 8.5ns ± 1% -90.14% (p=0.000 n=10+10) Atoi/Neg/26bit-4 183ns ± 1% 13ns ± 1% -92.77% (p=0.000 n=9+10) Atoi/Neg/31bit-4 223ns ± 0% 223ns ± 0% ~ (p=0.247 n=8+9) Fixes #20557 Change-Id: Ib6245d88cffd4b037419e2bf8e4a71b86c6d773f Reviewed-on: https://go-review.googlesource.com/44692 Reviewed-by: Robert Griesemer Reviewed-by: Keith Randall --- src/strconv/atoi.go | 28 +++++++++++ src/strconv/atoi_test.go | 101 +++++++++++++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/src/strconv/atoi.go b/src/strconv/atoi.go index e1ac42716c..bebed04820 100644 --- a/src/strconv/atoi.go +++ b/src/strconv/atoi.go @@ -201,6 +201,34 @@ func ParseInt(s string, base int, bitSize int) (i int64, err error) { // Atoi returns the result of ParseInt(s, 10, 0) converted to type int. func Atoi(s string) (int, error) { const fnAtoi = "Atoi" + + sLen := len(s) + if intSize == 32 && (0 < sLen && sLen < 10) || + intSize == 64 && (0 < sLen && sLen < 19) { + // Fast path for small integers that fit int type. + s0 := s + if s[0] == '-' || s[0] == '+' { + s = s[1:] + if len(s) < 1 { + return 0, &NumError{fnAtoi, s0, ErrSyntax} + } + } + + n := 0 + for _, ch := range []byte(s) { + ch -= '0' + if ch > 9 { + return 0, &NumError{fnAtoi, s0, ErrSyntax} + } + n = n*10 + int(ch) + } + if s0[0] == '-' { + n = -n + } + return n, nil + } + + // Slow path for invalid or big integers. i64, err := ParseInt(s, 10, 0) if nerr, ok := err.(*NumError); ok { nerr.Func = fnAtoi diff --git a/src/strconv/atoi_test.go b/src/strconv/atoi_test.go index 94844c7e10..e2f505a665 100644 --- a/src/strconv/atoi_test.go +++ b/src/strconv/atoi_test.go @@ -6,6 +6,7 @@ package strconv_test import ( "errors" + "fmt" "reflect" . "strconv" "testing" @@ -354,6 +355,37 @@ func TestParseInt(t *testing.T) { } } +func TestAtoi(t *testing.T) { + switch IntSize { + case 32: + for i := range parseInt32Tests { + test := &parseInt32Tests[i] + out, err := Atoi(test.in) + var testErr error + if test.err != nil { + testErr = &NumError{"Atoi", test.in, test.err.(*NumError).Err} + } + if int(test.out) != out || !reflect.DeepEqual(testErr, err) { + t.Errorf("Atoi(%q) = %v, %v want %v, %v", + test.in, out, err, test.out, testErr) + } + } + case 64: + for i := range parseInt64Tests { + test := &parseInt64Tests[i] + out, err := Atoi(test.in) + var testErr error + if test.err != nil { + testErr = &NumError{"Atoi", test.in, test.err.(*NumError).Err} + } + if test.out != int64(out) || !reflect.DeepEqual(testErr, err) { + t.Errorf("Atoi(%q) = %v, %v want %v, %v", + test.in, out, err, test.out, testErr) + } + } + } +} + func bitSizeErrStub(name string, bitSize int) error { return BitSizeError(name, "0", bitSize) } @@ -448,26 +480,67 @@ func TestNumError(t *testing.T) { } } -func BenchmarkAtoi(b *testing.B) { - for i := 0; i < b.N; i++ { - ParseInt("12345678", 10, 0) - } +func BenchmarkParseInt(b *testing.B) { + b.Run("Pos", func(b *testing.B) { + benchmarkParseInt(b, 1) + }) + b.Run("Neg", func(b *testing.B) { + benchmarkParseInt(b, -1) + }) } -func BenchmarkAtoiNeg(b *testing.B) { - for i := 0; i < b.N; i++ { - ParseInt("-12345678", 10, 0) - } +type benchCase struct { + name string + num int64 } -func BenchmarkAtoi64(b *testing.B) { - for i := 0; i < b.N; i++ { - ParseInt("12345678901234", 10, 64) +func benchmarkParseInt(b *testing.B, neg int) { + cases := []benchCase{ + {"7bit", 1<<7 - 1}, + {"26bit", 1<<26 - 1}, + {"31bit", 1<<31 - 1}, + {"56bit", 1<<56 - 1}, + {"63bit", 1<<63 - 1}, + } + for _, cs := range cases { + b.Run(cs.name, func(b *testing.B) { + s := fmt.Sprintf("%d", cs.num*int64(neg)) + for i := 0; i < b.N; i++ { + out, _ := ParseInt(s, 10, 64) + BenchSink += int(out) + } + }) } } -func BenchmarkAtoi64Neg(b *testing.B) { - for i := 0; i < b.N; i++ { - ParseInt("-12345678901234", 10, 64) +func BenchmarkAtoi(b *testing.B) { + b.Run("Pos", func(b *testing.B) { + benchmarkAtoi(b, 1) + }) + b.Run("Neg", func(b *testing.B) { + benchmarkAtoi(b, -1) + }) +} + +func benchmarkAtoi(b *testing.B, neg int) { + cases := []benchCase{ + {"7bit", 1<<7 - 1}, + {"26bit", 1<<26 - 1}, + {"31bit", 1<<31 - 1}, + } + if IntSize == 64 { + cases = append(cases, []benchCase{ + {"56bit", 1<<56 - 1}, + {"63bit", 1<<63 - 1}, + }...) + } + for _, cs := range cases { + b.Run(cs.name, func(b *testing.B) { + s := fmt.Sprintf("%d", cs.num*int64(neg)) + for i := 0; i < b.N; i++ { + out, _ := Atoi(s) + BenchSink += out + } + }) } } -- 2.48.1