Why is strconv.ParseUint so slow compared to strconv.Atoi?

1.2k views Asked by At

I'm benchmarking unmarshaling from string to int and uint with this code:

package main

import (
    "strconv"
    "testing"
)

func BenchmarkUnmarshalInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        UnmarshalInt("123456")
    }
}

func BenchmarkUnmarshalUint(b *testing.B) {
    for i := 0; i < b.N; i++ {
        UnmarshalUint("123456")
    }
}

func UnmarshalInt(v string) int {
    i, _ := strconv.Atoi(v)
    return i
}

func UnmarshalUint(v string) uint {
    i, _ := strconv.ParseUint(v, 10, 64)
    return uint(i)
}

Result:

Running tool: C:\Go\bin\go.exe test -benchmem -run=^$ myBench/main -bench .

goos: windows
goarch: amd64
pkg: myBench/main
BenchmarkUnmarshalInt-8     99994166            11.7 ns/op         0 B/op          0 allocs/op
BenchmarkUnmarshalUint-8    54550413            21.0 ns/op         0 B/op          0 allocs/op

Is it possible that the second (uint) is almost twice as slow as the first (int)?

1

There are 1 answers

8
Paul Hankin On

Yes, it's possible. strconv.Atoi has a fast path when the input string length is less than 19 (or 10 if int is 32 bit). This allows it to be a lot faster because it doesn't need to check for overflow.

If you change your test number to "1234567890123456789" (assuming 64 bit int), then your int benchmark is slightly slower than the uint benchmark because the fast path can't be used. On my machine, it takes 37.6 ns/op for the signed version vs 31.5 ns/op for the unsigned version.

Here's the modified benchmark code (note I added a variable that sums up the parsed results, just in case the compiler got clever and optimized it away).

package main

import (
        "fmt"
        "strconv"
        "testing"
)

const X = "1234567890123456789"

func BenchmarkUnmarshalInt(b *testing.B) {
        var T int
        for i := 0; i < b.N; i++ {
                T += UnmarshalInt(X)
        }
        fmt.Println(T)
}

func BenchmarkUnmarshalUint(b *testing.B) {
        var T uint
        for i := 0; i < b.N; i++ {
                T += UnmarshalUint(X)
        }
        fmt.Println(T)
}

func UnmarshalInt(v string) int {
        i, _ := strconv.Atoi(v)
        return i
}

func UnmarshalUint(v string) uint {
        i, _ := strconv.ParseUint(v, 10, 64)
        return uint(i)
}

For reference, the code for strconv.Atoi in the standard library is currently as follows:

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, big, or underscored integers.
    i64, err := ParseInt(s, 10, 0)
    if nerr, ok := err.(*NumError); ok {
        nerr.Func = fnAtoi
    }
    return int(i64), err
}