]> Cypherpunks repositories - gostls13.git/commitdiff
strconv: optimize Atoi for common case
authorAliaksandr Valialkin <valyala@gmail.com>
Fri, 2 Jun 2017 16:16:19 +0000 (19:16 +0300)
committerRobert Griesemer <gri@golang.org>
Fri, 25 Aug 2017 10:17:39 +0000 (10:17 +0000)
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 <gri@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
src/strconv/atoi.go
src/strconv/atoi_test.go

index e1ac42716c299c0a3abf99021ffc6f9d48544459..bebed048204fb0eaeac57bf625987c7c2d5a2269 100644 (file)
@@ -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
index 94844c7e103ce073fb945f57a67b7c0ff2293aba..e2f505a66516c8019acf90608e7bd159cccd8210 100644 (file)
@@ -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
+                       }
+               })
        }
 }