Skip to content

Commit 46aa9f5

Browse files
valyalagriesemer
authored andcommitted
strconv: optimize Atoi for common case
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 <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 180bfc4 commit 46aa9f5

File tree

2 files changed

+115
-14
lines changed

2 files changed

+115
-14
lines changed

src/strconv/atoi.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,34 @@ func ParseInt(s string, base int, bitSize int) (i int64, err error) {
201201
// Atoi returns the result of ParseInt(s, 10, 0) converted to type int.
202202
func Atoi(s string) (int, error) {
203203
const fnAtoi = "Atoi"
204+
205+
sLen := len(s)
206+
if intSize == 32 && (0 < sLen && sLen < 10) ||
207+
intSize == 64 && (0 < sLen && sLen < 19) {
208+
// Fast path for small integers that fit int type.
209+
s0 := s
210+
if s[0] == '-' || s[0] == '+' {
211+
s = s[1:]
212+
if len(s) < 1 {
213+
return 0, &NumError{fnAtoi, s0, ErrSyntax}
214+
}
215+
}
216+
217+
n := 0
218+
for _, ch := range []byte(s) {
219+
ch -= '0'
220+
if ch > 9 {
221+
return 0, &NumError{fnAtoi, s0, ErrSyntax}
222+
}
223+
n = n*10 + int(ch)
224+
}
225+
if s0[0] == '-' {
226+
n = -n
227+
}
228+
return n, nil
229+
}
230+
231+
// Slow path for invalid or big integers.
204232
i64, err := ParseInt(s, 10, 0)
205233
if nerr, ok := err.(*NumError); ok {
206234
nerr.Func = fnAtoi

src/strconv/atoi_test.go

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package strconv_test
66

77
import (
88
"errors"
9+
"fmt"
910
"reflect"
1011
. "strconv"
1112
"testing"
@@ -354,6 +355,37 @@ func TestParseInt(t *testing.T) {
354355
}
355356
}
356357

358+
func TestAtoi(t *testing.T) {
359+
switch IntSize {
360+
case 32:
361+
for i := range parseInt32Tests {
362+
test := &parseInt32Tests[i]
363+
out, err := Atoi(test.in)
364+
var testErr error
365+
if test.err != nil {
366+
testErr = &NumError{"Atoi", test.in, test.err.(*NumError).Err}
367+
}
368+
if int(test.out) != out || !reflect.DeepEqual(testErr, err) {
369+
t.Errorf("Atoi(%q) = %v, %v want %v, %v",
370+
test.in, out, err, test.out, testErr)
371+
}
372+
}
373+
case 64:
374+
for i := range parseInt64Tests {
375+
test := &parseInt64Tests[i]
376+
out, err := Atoi(test.in)
377+
var testErr error
378+
if test.err != nil {
379+
testErr = &NumError{"Atoi", test.in, test.err.(*NumError).Err}
380+
}
381+
if test.out != int64(out) || !reflect.DeepEqual(testErr, err) {
382+
t.Errorf("Atoi(%q) = %v, %v want %v, %v",
383+
test.in, out, err, test.out, testErr)
384+
}
385+
}
386+
}
387+
}
388+
357389
func bitSizeErrStub(name string, bitSize int) error {
358390
return BitSizeError(name, "0", bitSize)
359391
}
@@ -448,26 +480,67 @@ func TestNumError(t *testing.T) {
448480
}
449481
}
450482

451-
func BenchmarkAtoi(b *testing.B) {
452-
for i := 0; i < b.N; i++ {
453-
ParseInt("12345678", 10, 0)
454-
}
483+
func BenchmarkParseInt(b *testing.B) {
484+
b.Run("Pos", func(b *testing.B) {
485+
benchmarkParseInt(b, 1)
486+
})
487+
b.Run("Neg", func(b *testing.B) {
488+
benchmarkParseInt(b, -1)
489+
})
455490
}
456491

457-
func BenchmarkAtoiNeg(b *testing.B) {
458-
for i := 0; i < b.N; i++ {
459-
ParseInt("-12345678", 10, 0)
460-
}
492+
type benchCase struct {
493+
name string
494+
num int64
461495
}
462496

463-
func BenchmarkAtoi64(b *testing.B) {
464-
for i := 0; i < b.N; i++ {
465-
ParseInt("12345678901234", 10, 64)
497+
func benchmarkParseInt(b *testing.B, neg int) {
498+
cases := []benchCase{
499+
{"7bit", 1<<7 - 1},
500+
{"26bit", 1<<26 - 1},
501+
{"31bit", 1<<31 - 1},
502+
{"56bit", 1<<56 - 1},
503+
{"63bit", 1<<63 - 1},
504+
}
505+
for _, cs := range cases {
506+
b.Run(cs.name, func(b *testing.B) {
507+
s := fmt.Sprintf("%d", cs.num*int64(neg))
508+
for i := 0; i < b.N; i++ {
509+
out, _ := ParseInt(s, 10, 64)
510+
BenchSink += int(out)
511+
}
512+
})
466513
}
467514
}
468515

469-
func BenchmarkAtoi64Neg(b *testing.B) {
470-
for i := 0; i < b.N; i++ {
471-
ParseInt("-12345678901234", 10, 64)
516+
func BenchmarkAtoi(b *testing.B) {
517+
b.Run("Pos", func(b *testing.B) {
518+
benchmarkAtoi(b, 1)
519+
})
520+
b.Run("Neg", func(b *testing.B) {
521+
benchmarkAtoi(b, -1)
522+
})
523+
}
524+
525+
func benchmarkAtoi(b *testing.B, neg int) {
526+
cases := []benchCase{
527+
{"7bit", 1<<7 - 1},
528+
{"26bit", 1<<26 - 1},
529+
{"31bit", 1<<31 - 1},
530+
}
531+
if IntSize == 64 {
532+
cases = append(cases, []benchCase{
533+
{"56bit", 1<<56 - 1},
534+
{"63bit", 1<<63 - 1},
535+
}...)
536+
}
537+
for _, cs := range cases {
538+
b.Run(cs.name, func(b *testing.B) {
539+
s := fmt.Sprintf("%d", cs.num*int64(neg))
540+
for i := 0; i < b.N; i++ {
541+
out, _ := Atoi(s)
542+
BenchSink += out
543+
}
544+
})
472545
}
473546
}

0 commit comments

Comments
 (0)