Skip to content

Commit

Permalink
improve performance and reduce allocations of most UUID methods
Browse files Browse the repository at this point in the history
This commit improves the performance and reduces the number of allocs of
most UUID methods and adds a new UUID.Parse() method for parsing string
encoded UUIDs.

Parsing string encoded UUIDs is now 2x faster and no longer allocates.
The NullUUID MarshalJSON() and UnmarshalJSON() methods have also been
improved and no longer call out to json.Unmarshal. The UUID.Format
method has been improved for common cases.

Benchmark results:
```
goos: linux
goarch: amd64
pkg: github.com/gofrs/uuid
cpu: Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz

name                        old time/op    new time/op    delta
UnmarshalText/canonical-16    47.7ns ± 0%    22.3ns ± 0%   -53.26%  (p=0.000 n=8+10)
UnmarshalText/urn-16          47.8ns ± 1%    22.3ns ± 0%   -53.32%  (p=0.000 n=10+10)
UnmarshalText/braced-16       47.8ns ± 1%    22.3ns ± 0%   -53.34%  (p=0.000 n=9+9)
ParseV4-16                    86.5ns ±10%    22.6ns ± 0%   -73.85%  (p=0.000 n=9+8)
NullMarshalJSON/Valid-16       308ns ±21%      72ns ±12%   -76.71%  (p=0.000 n=10+10)
NullMarshalJSON/Invalid-16    41.8ns ± 2%     1.4ns ± 0%   -96.59%  (p=0.000 n=10+8)
Format/s-16                    151ns ± 3%     143ns ± 7%    -5.18%  (p=0.003 n=10+10)
Format/S-16                    305ns ± 2%     161ns ± 2%   -47.38%  (p=0.000 n=10+9)
Format/q-16                    217ns ±12%     144ns ± 6%   -33.52%  (p=0.000 n=10+10)
Format/x-16                    170ns ±13%     123ns ± 1%   -27.95%  (p=0.000 n=10+10)
Format/X-16                    324ns ±11%     148ns ± 1%   -54.15%  (p=0.000 n=10+10)
Format/v-16                    156ns ± 4%     142ns ± 5%    -8.68%  (p=0.000 n=10+10)
Format/+v-16                   155ns ± 3%     142ns ± 6%    -8.67%  (p=0.000 n=10+10)
Format/#v-16                   894ns ± 1%     847ns ± 1%    -5.22%  (p=0.000 n=10+9)
String-16                     70.1ns ±36%    70.4ns ±29%      ~     (p=0.971 n=10+10)
FromBytes-16                  1.81ns ± 0%    1.81ns ± 0%    -0.15%  (p=0.010 n=8+9)
FromString/canonical-16       94.3ns ±20%    23.3ns ± 1%   -75.25%  (p=0.000 n=10+10)
FromString/urn-16             93.7ns ±11%    23.8ns ± 1%   -74.66%  (p=0.000 n=10+8)
FromString/braced-16          87.1ns ± 5%    23.5ns ± 1%   -73.03%  (p=0.000 n=9+10)
MarshalBinary-16              0.20ns ± 3%    0.20ns ± 1%      ~     (p=0.922 n=10+9)
MarshalText-16                 115ns ±25%      22ns ± 1%   -80.66%  (p=0.000 n=10+10)

name                        old alloc/op   new alloc/op   delta
UnmarshalText/canonical-16     0.00B          0.00B           ~     (all equal)
UnmarshalText/urn-16           0.00B          0.00B           ~     (all equal)
UnmarshalText/braced-16        0.00B          0.00B           ~     (all equal)
ParseV4-16                     48.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
NullMarshalJSON/Valid-16        160B ± 0%       48B ± 0%   -70.00%  (p=0.000 n=10+10)
NullMarshalJSON/Invalid-16     8.00B ± 0%     0.00B       -100.00%  (p=0.000 n=10+10)
Format/s-16                    48.0B ± 0%     48.0B ± 0%      ~     (all equal)
Format/S-16                    96.0B ± 0%     48.0B ± 0%   -50.00%  (p=0.000 n=10+10)
Format/q-16                    96.0B ± 0%     48.0B ± 0%   -50.00%  (p=0.000 n=10+10)
Format/x-16                    64.0B ± 0%     32.0B ± 0%   -50.00%  (p=0.000 n=10+10)
Format/X-16                     112B ± 0%       32B ± 0%   -71.43%  (p=0.000 n=10+10)
Format/v-16                    48.0B ± 0%     48.0B ± 0%      ~     (all equal)
Format/+v-16                   48.0B ± 0%     48.0B ± 0%      ~     (all equal)
Format/#v-16                    128B ± 0%       16B ± 0%   -87.50%  (p=0.000 n=10+10)
String-16                      48.0B ± 0%     48.0B ± 0%      ~     (all equal)
FromBytes-16                   0.00B          0.00B           ~     (all equal)
FromString/canonical-16        48.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
FromString/urn-16              48.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
FromString/braced-16           48.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
MarshalBinary-16               0.00B          0.00B           ~     (all equal)
MarshalText-16                 96.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)

name                        old allocs/op  new allocs/op  delta
UnmarshalText/canonical-16      0.00           0.00           ~     (all equal)
UnmarshalText/urn-16            0.00           0.00           ~     (all equal)
UnmarshalText/braced-16         0.00           0.00           ~     (all equal)
ParseV4-16                      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
NullMarshalJSON/Valid-16        4.00 ± 0%      1.00 ± 0%   -75.00%  (p=0.000 n=10+10)
NullMarshalJSON/Invalid-16      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
Format/s-16                     1.00 ± 0%      1.00 ± 0%      ~     (all equal)
Format/S-16                     2.00 ± 0%      1.00 ± 0%   -50.00%  (p=0.000 n=10+10)
Format/q-16                     2.00 ± 0%      1.00 ± 0%   -50.00%  (p=0.000 n=10+10)
Format/x-16                     2.00 ± 0%      1.00 ± 0%   -50.00%  (p=0.000 n=10+10)
Format/X-16                     3.00 ± 0%      1.00 ± 0%   -66.67%  (p=0.000 n=10+10)
Format/v-16                     1.00 ± 0%      1.00 ± 0%      ~     (all equal)
Format/+v-16                    1.00 ± 0%      1.00 ± 0%      ~     (all equal)
Format/#v-16                    2.00 ± 0%      1.00 ± 0%   -50.00%  (p=0.000 n=10+10)
String-16                       1.00 ± 0%      1.00 ± 0%      ~     (all equal)
FromBytes-16                    0.00           0.00           ~     (all equal)
FromString/canonical-16         1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
FromString/urn-16               1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
FromString/braced-16            1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
MarshalBinary-16                0.00           0.00           ~     (all equal)
MarshalText-16                  2.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
```
  • Loading branch information
charlievieth committed Dec 5, 2021
1 parent 0c84a43 commit 5f181ae
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 160 deletions.
199 changes: 111 additions & 88 deletions codec.go
Expand Up @@ -22,8 +22,7 @@
package uuid

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
)

Expand All @@ -45,11 +44,77 @@ func FromBytesOrNil(input []byte) UUID {
return uuid
}

var errInvalidFormat = errors.New("uuid: invalid UUID format")

func fromHexChar(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 255
}

// Parse parses the UUID stored in the string text. Parsing and supported
// formats are the same as UnmarshalText.
func (u *UUID) Parse(s string) error {
switch len(s) {
case 32: // hash
case 36: // canonical
case 34, 38:
if s[0] != '{' || s[len(s)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
}
s = s[1 : len(s)-1]
case 41, 45:
if s[:9] != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s[:9])
}
s = s[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(s), s)
}
// canonical
if len(s) == 36 {
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
}
for i, x := range [16]byte{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v1 := fromHexChar(s[x])
v2 := fromHexChar(s[x+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i] = (v1 << 4) | v2
}
return nil
}
// hash like
for i := 0; i < 32; i += 2 {
v1 := fromHexChar(s[i])
v2 := fromHexChar(s[i+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
return nil
}

// FromString returns a UUID parsed from the input string.
// Input is expected in a form accepted by UnmarshalText.
func FromString(input string) (UUID, error) {
u := UUID{}
err := u.UnmarshalText([]byte(input))
func FromString(text string) (UUID, error) {
var u UUID
err := u.Parse(text)
return u, err
}

Expand All @@ -66,7 +131,9 @@ func FromStringOrNil(input string) UUID {
// MarshalText implements the encoding.TextMarshaler interface.
// The encoding is the same as returned by the String() method.
func (u UUID) MarshalText() ([]byte, error) {
return []byte(u.String()), nil
var buf [36]byte
encodeHex(buf[:], u)
return buf[:], nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
Expand Down Expand Up @@ -103,96 +170,52 @@ func (u UUID) MarshalText() ([]byte, error) {
// braced := '{' plain '}' | '{' hashlike '}'
// urn := URN ':' UUID-NID ':' plain
//
func (u *UUID) UnmarshalText(text []byte) error {
switch len(text) {
case 32:
return u.decodeHashLike(text)
func (u *UUID) UnmarshalText(b []byte) error {
switch len(b) {
case 32: // hash
case 36: // canonical
case 34, 38:
return u.decodeBraced(text)
case 36:
return u.decodeCanonical(text)
if b[0] != '{' || b[len(b)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
}
b = b[1 : len(b)-1]
case 41, 45:
return u.decodeURN(text)
if string(b[:9]) != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b[:9])
}
b = b[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(text), text)
}
}

// decodeCanonical decodes UUID strings that are formatted as defined in RFC-4122 (section 3):
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8".
func (u *UUID) decodeCanonical(t []byte) error {
if t[8] != '-' || t[13] != '-' || t[18] != '-' || t[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", t)
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(b), b)
}

src := t
dst := u[:]

for i, byteGroup := range byteGroups {
if i > 0 {
src = src[1:] // skip dash
if len(b) == 36 {
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
}
_, err := hex.Decode(dst[:byteGroup/2], src[:byteGroup])
if err != nil {
return err
for i, x := range [16]byte{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v1 := fromHexChar(b[x])
v2 := fromHexChar(b[x+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i] = (v1 << 4) | v2
}
src = src[byteGroup:]
dst = dst[byteGroup/2:]
}

return nil
}

// decodeHashLike decodes UUID strings that are using the following format:
// "6ba7b8109dad11d180b400c04fd430c8".
func (u *UUID) decodeHashLike(t []byte) error {
src := t[:]
dst := u[:]

_, err := hex.Decode(dst, src)
return err
}

// decodeBraced decodes UUID strings that are using the following formats:
// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}"
// "{6ba7b8109dad11d180b400c04fd430c8}".
func (u *UUID) decodeBraced(t []byte) error {
l := len(t)

if t[0] != '{' || t[l-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", t)
return nil
}

return u.decodePlain(t[1 : l-1])
}

// decodeURN decodes UUID strings that are using the following formats:
// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
// "urn:uuid:6ba7b8109dad11d180b400c04fd430c8".
func (u *UUID) decodeURN(t []byte) error {
total := len(t)

urnUUIDPrefix := t[:9]

if !bytes.Equal(urnUUIDPrefix, urnPrefix) {
return fmt.Errorf("uuid: incorrect UUID format in string %q", t)
}

return u.decodePlain(t[9:total])
}

// decodePlain decodes UUID strings that are using the following formats:
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8" or in hash-like format
// "6ba7b8109dad11d180b400c04fd430c8".
func (u *UUID) decodePlain(t []byte) error {
switch len(t) {
case 32:
return u.decodeHashLike(t)
case 36:
return u.decodeCanonical(t)
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(t), t)
for i := 0; i < 32; i += 2 {
v1 := fromHexChar(b[i])
v2 := fromHexChar(b[i+1])
if v1|v2 == 255 {
return errInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
return nil
}

// MarshalBinary implements the encoding.BinaryMarshaler interface.
Expand Down

0 comments on commit 5f181ae

Please sign in to comment.