Skip to content

Commit

Permalink
update benchmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
jackspirou committed Jan 11, 2025
1 parent bc14453 commit 5e289c0
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 105 deletions.
53 changes: 35 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ type Key string
```
<a name="Encode"></a>
### func [Encode](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L151>)
### func [Encode](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L154>)
```go
func Encode(uuid string, opts ...Option) (Key, error)
Expand All @@ -197,7 +197,7 @@ func Encode(uuid string, opts ...Option) (Key, error)
Encode will encode a given UUID string into a Key. It pre\-allocates the exact string capacity needed for better performance.
<a name="EncodeBytes"></a>
### func [EncodeBytes](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L196>)
### func [EncodeBytes](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L212>)
```go
func EncodeBytes(uuid [16]byte, opts ...Option) (Key, error)
Expand All @@ -215,7 +215,7 @@ func Parse(key string) (Key, error)
Parse converts a Key formatted string into a Key type.
<a name="Key.Bytes"></a>
### func \(Key\) [Bytes](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L280>)
### func \(Key\) [Bytes](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L314>)
```go
func (k Key) Bytes() ([16]byte, error)
Expand All @@ -224,7 +224,7 @@ func (k Key) Bytes() ([16]byte, error)
Bytes converts a Key to a \[16\]byte UUID.
<a name="Key.Decode"></a>
### func \(Key\) [Decode](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L221>)
### func \(Key\) [Decode](<https://github.com/agentstation/uuidkey/blob/master/uuidkey.go#L256>)
```go
func (k Key) Decode() (string, error)
Expand Down Expand Up @@ -321,28 +321,45 @@ Benchmarking, Testing, & Coverage
## Benchmarks
> **Note:** These benchmarks were run on an Apple M2 Max CPU with 12 cores (8 performance and 4 efficiency) and 32 GB of memory, running macOS 14.6.1.
> **Note:** These benchmarks were run on an Apple M2 Max CPU with 12 cores (8 performance and 4 efficiency) and 32 GB of memory, running macOS 14.6.1. The results are not representative of all systems, but should give you a general idea of the performance of the package. I was running other processes on the machine while running the benchmarks.
*Your mileage may vary.*
```sh
make bench
devbox ➜ make bench
Running go benchmarks...
go test ./... -tags=bench -bench=.
goos: darwin
goarch: arm64
pkg: github.com/agentstation/uuidkey
cpu: Apple M2 Max
BenchmarkValidate-12 33527211 35.72 ns/op
BenchmarkParse-12 32329798 36.96 ns/op
BenchmarkFromKey-12 4886846 250.6 ns/op
BenchmarkEncode-12 3151844 377.0 ns/op
BenchmarkDecode-12 5587066 216.7 ns/op
BenchmarkValidateInvalid-12 1000000000 0.2953 ns/op
BenchmarkParseValid-12 32424325 36.89 ns/op
BenchmarkParseInvalid-12 70131522 17.01 ns/op
BenchmarkUUIDValid-12 4693452 247.2 ns/op
BenchmarkUUIDInvalid-12 70141429 16.92 ns/op
BenchmarkValidate-12 40281177 29.69 ns/op
BenchmarkValidateInvalid-12 819883306 1.496 ns/op
BenchmarkParse-12 38547507 30.36 ns/op
BenchmarkParseInvalid-12 662246173 1.798 ns/op
BenchmarkUUID-12 4692667 251.8 ns/op
BenchmarkUUIDInvalid-12 66003271 17.17 ns/op
BenchmarkEncode-12 7226437 164.8 ns/op
BenchmarkDecode-12 5326918 229.8 ns/op
BenchmarkBytes-12 5419532 220.6 ns/op
BenchmarkEncodeBytes-12 12422665 95.78 ns/op
BenchmarkValidateWithHyphens-12 38696102 30.33 ns/op
BenchmarkValidateWithoutHyphens-12 38651440 29.55 ns/op
BenchmarkParseWithHyphens-12 38664464 29.83 ns/op
BenchmarkParseWithoutHyphens-12 38533995 30.15 ns/op
BenchmarkEncodeWithHyphens-12 6993826 167.3 ns/op
BenchmarkEncodeWithoutHyphens-12 7191475 168.0 ns/op
BenchmarkDecodeWithHyphens-12 5343776 221.3 ns/op
BenchmarkDecodeWithoutHyphens-12 5354410 222.1 ns/op
BenchmarkBytesWithHyphens-12 5379320 218.8 ns/op
BenchmarkBytesWithoutHyphens-12 5454608 218.7 ns/op
BenchmarkEncodeBytesWithHyphens-12 12571252 94.46 ns/op
BenchmarkEncodeBytesWithoutHyphens-12 12327489 96.53 ns/op
BenchmarkString-12 1000000000 0.2878 ns/op
BenchmarkValidateInvalidFormat-12 825361526 1.444 ns/op
BenchmarkParseInvalidFormat-12 659288433 1.771 ns/op
BenchmarkDecodeInvalidFormat-12 10191313 114.9 ns/op
BenchmarkEncodeInvalidUUID-12 10994308 103.3 ns/op
BenchmarkBytesInvalidFormat-12 10311207 116.2 ns/op
PASS
ok github.com/agentstation/uuidkey 13.365s
ok github.com/agentstation/uuidkey 36.990s
```
223 changes: 136 additions & 87 deletions uuidkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,22 @@ var WithoutHyphens Option = func(c *config) {
c.hyphens = false
}

// encode will convert your given int64 into base32 crockford encoding format
func encode(n uint64) string {
encoded := crock32.Encode(n)
padding := 7 - len(encoded)
return strings.ToUpper((strings.Repeat("0", padding) + encoded))
}

// decode will convert your given string into original UUID part string
func decode(s string) string {
i, _ := crock32.Decode(s)

var builder strings.Builder
builder.Grow(8) // We know the result will be 8 chars

decoded := strconv.FormatUint(i, 16)
padding := 8 - len(decoded)
return (strings.Repeat("0", padding) + decoded)

for i := 0; i < padding; i++ {
builder.WriteByte('0')
}
builder.WriteString(decoded)

return builder.String()
}

// Encode will encode a given UUID string into a Key.
Expand All @@ -155,66 +158,98 @@ func Encode(uuid string, opts ...Option) (Key, error) {
return "", fmt.Errorf("invalid UUID length: expected %d characters, got %d", UUIDLength, len(uuid))
}

// Pre-allocate a single slice for the result
result := make([]byte, KeyLengthWithHyphens)

// Process parts directly into the result slice
// UUID format: 8-4-4-4-12 = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
processUUIDPart(uuid[0:8], result[0:7]) // First 8 chars
processUUIDPart(uuid[9:13]+uuid[14:18], result[8:15]) // 4-4 middle section
processUUIDPart(uuid[19:23]+uuid[24:28], result[16:23]) // 4-4 middle section
processUUIDPart(uuid[28:36], result[24:31]) // Last 12 chars
// Pre-allocate a strings.Builder with exact capacity
var builder strings.Builder
if options.hyphens {
builder.Grow(KeyLengthWithHyphens)
} else {
builder.Grow(KeyLengthWithoutHyphens)
}

// Add hyphens if needed
// Process each part
processAndWritePart(&builder, uuid[0:8])
if options.hyphens {
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[9:13]+uuid[14:18])
if options.hyphens {
result[7] = '-'
result[15] = '-'
result[23] = '-'
return Key(result), nil
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[19:23]+uuid[24:28])
if options.hyphens {
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[28:36])

// Return without hyphens
return Key(append(result[:7], append(result[8:15], append(result[16:23], result[24:31]...)...)...)), nil
return Key(builder.String()), nil
}

// processUUIDPart converts a hex UUID part directly to base32 and writes to the destination
func processUUIDPart(src string, dst []byte) {
func processAndWritePart(builder *strings.Builder, src string) {
n, _ := strconv.ParseUint(src, 16, 64)
encoded := crock32.Encode(n)
padding := 7 - len(encoded)

// Write padding zeros
for i := 0; i < padding; i++ {
dst[i] = '0'
builder.WriteByte('0')
}

// Write encoded part
copy(dst[padding:], strings.ToUpper(encoded))
// Write encoded part in uppercase
for i := 0; i < len(encoded); i++ {
builder.WriteByte(toUpper(encoded[i]))
}
}

// toUpper converts a single byte to uppercase if it's a lowercase letter
func toUpper(c byte) byte {
if c >= 'a' && c <= 'z' {
return c - 32
}
return c
}

// EncodeBytes encodes a [16]byte UUID into a Key.
func EncodeBytes(uuid [16]byte, opts ...Option) (Key, error) {
// apply the options to the default options
options := apply(opts...)

// Convert byte groups directly to uint64
// Each group of 4 bytes is combined into a single uint64
n1 := uint64(uuid[0])<<24 | uint64(uuid[1])<<16 | uint64(uuid[2])<<8 | uint64(uuid[3])
n2 := uint64(uuid[4])<<24 | uint64(uuid[5])<<16 | uint64(uuid[6])<<8 | uint64(uuid[7])
n3 := uint64(uuid[8])<<24 | uint64(uuid[9])<<16 | uint64(uuid[10])<<8 | uint64(uuid[11])
n4 := uint64(uuid[12])<<24 | uint64(uuid[13])<<16 | uint64(uuid[14])<<8 | uint64(uuid[15])

// Encode each uint64 into base32 crockford encoding format
e1 := encode(n1) // Encodes bytes 0-3
e2 := encode(n2) // Encodes bytes 4-7
e3 := encode(n3) // Encodes bytes 8-11
e4 := encode(n4) // Encodes bytes 12-15
var builder strings.Builder
if options.hyphens {
builder.Grow(KeyLengthWithHyphens)
} else {
builder.Grow(KeyLengthWithoutHyphens)
}

// Build and return key
// Process each 4-byte group
writeEncodedPart(&builder, uint64(uuid[0])<<24|uint64(uuid[1])<<16|uint64(uuid[2])<<8|uint64(uuid[3]))
if options.hyphens {
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[4])<<24|uint64(uuid[5])<<16|uint64(uuid[6])<<8|uint64(uuid[7]))
if options.hyphens {
return Key(e1 + "-" + e2 + "-" + e3 + "-" + e4), nil
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[8])<<24|uint64(uuid[9])<<16|uint64(uuid[10])<<8|uint64(uuid[11]))
if options.hyphens {
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[12])<<24|uint64(uuid[13])<<16|uint64(uuid[14])<<8|uint64(uuid[15]))

return Key(builder.String()), nil
}

func writeEncodedPart(builder *strings.Builder, n uint64) {
encoded := crock32.Encode(n)
padding := 7 - len(encoded)

// Write padding zeros
for i := 0; i < padding; i++ {
builder.WriteByte('0')
}

// Write encoded part in uppercase
for i := 0; i < len(encoded); i++ {
builder.WriteByte(toUpper(encoded[i]))
}
return Key(e1 + e2 + e3 + e4), nil
}

// Decode will decode a given Key into a UUID string with basic length validation.
Expand All @@ -224,26 +259,28 @@ func (k Key) Decode() (string, error) {
length := len(k)
if length != KeyLengthWithoutHyphens {
if length != KeyLengthWithHyphens {
return "", fmt.Errorf("invalid Key length: expected %d or %d characters, got %d", KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
return "", fmt.Errorf("invalid Key length: expected %d or %d characters, got %d",
KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
}
hyphens = true
}

// convert the type from a Key to string
key := string(k)
var s1, s2, s3, s4 string
var builder strings.Builder
builder.Grow(UUIDLength) // Pre-allocate exact size needed: 36 bytes

// select the 4 parts of the key string
var s1, s2, s3, s4 string
s := string(k)
if hyphens {
s1 = key[0:7] // [38QARV0]-1ET0G6Z-2CJD9VA-2ZZAR0X
s2 = key[8:15] // 38QARV0-[1ET0G6Z]-2CJD9VA-2ZZAR0X
s3 = key[16:23] // 38QARV0-1ET0G6Z-[2CJD9VA]-2ZZAR0X
s4 = key[24:31] // 38QARV0-1ET0G6Z-2CJD9VA-[2ZZAR0X]
s1 = s[0:7] // [38QARV0]-1ET0G6Z-2CJD9VA-2ZZAR0X
s2 = s[8:15] // 38QARV0-[1ET0G6Z]-2CJD9VA-2ZZAR0X
s3 = s[16:23] // 38QARV0-1ET0G6Z-[2CJD9VA]-2ZZAR0X
s4 = s[24:31] // 38QARV0-1ET0G6Z-2CJD9VA-[2ZZAR0X]
} else {
s1 = key[0:7] // [38QARV0]1ET0G6Z2CJD9VA2ZZAR0X
s2 = key[7:14] // 38QARV0[1ET0G6Z]2CJD9VA2ZZAR0X
s3 = key[14:21] // 38QARV01ET0G6Z[2CJD9VA]2ZZAR0X
s4 = key[21:28] // 38QARV01ET0G6Z2CJD9VA[2ZZAR0X]
s1 = s[0:7] // [38QARV0]1ET0G6Z2CJD9VA2ZZAR0X
s2 = s[7:14] // 38QARV0[1ET0G6Z]2CJD9VA2ZZAR0X
s3 = s[14:21] // 38QARV01ET0G6Z[2CJD9VA]2ZZAR0X
s4 = s[21:28] // 38QARV01ET0G6Z2CJD9VA[2ZZAR0X]
}

// decode each string part into original UUID part string
Expand All @@ -258,9 +295,6 @@ func (k Key) Decode() (string, error) {
n3a := n3[0:4]
n3b := n3[4:8]

var builder strings.Builder
builder.Grow(UUIDLength) // Pre-allocate exact size needed

// Write parts with proper formatting
builder.WriteString(n1)
builder.WriteByte('-')
Expand All @@ -278,42 +312,57 @@ func (k Key) Decode() (string, error) {

// Bytes converts a Key to a [16]byte UUID.
func (k Key) Bytes() ([16]byte, error) {
// convert the type from a Key to string
keyStr := string(k)

// determine if we should expect hyphens given the length of the key
hyphens := false
length := len(keyStr)
if length != KeyLengthWithoutHyphens {
if length != KeyLengthWithHyphens {
return [16]byte{}, fmt.Errorf("invalid Key length: expected %d or %d characters, got %d", KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
}
hyphens = true
length := len(k)
if length != KeyLengthWithoutHyphens && length != KeyLengthWithHyphens {
return [16]byte{}, fmt.Errorf("invalid Key length: expected %d or %d characters, got %d",
KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
}

// initialize the UUID array
hyphens := length == KeyLengthWithHyphens
var uuid [16]byte
var err error
var n uint64

// select the 4 parts of the key string
keyParts := [4]string{keyStr[0:7], keyStr[7:14], keyStr[14:21], keyStr[21:28]}
// Avoid string conversion by working directly with the Key type
if hyphens {
keyParts = [4]string{keyStr[0:7], keyStr[8:15], keyStr[16:23], keyStr[24:31]}
if err = processByteGroup(k[0:7], &uuid, 0); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[8:15], &uuid, 4); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[16:23], &uuid, 8); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[24:31], &uuid, 12); err != nil {
return [16]byte{}, err
}
} else {
if err = processByteGroup(k[0:7], &uuid, 0); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[7:14], &uuid, 4); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[14:21], &uuid, 8); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(k[21:28], &uuid, 12); err != nil {
return [16]byte{}, err
}
}

// Process each part of the key
for i, part := range keyParts {
if n, err = crock32.Decode(strings.ToLower(part)); err != nil {
return [16]byte{}, fmt.Errorf("failed to decode Key part: %v", err)
}
return uuid, nil
}

// Write 4 bytes for each part
uuid[i*4] = byte(n >> 24)
uuid[i*4+1] = byte(n >> 16)
uuid[i*4+2] = byte(n >> 8)
uuid[i*4+3] = byte(n)
func processByteGroup(part Key, uuid *[16]byte, offset int) error {
n, err := crock32.Decode(strings.ToLower(string(part)))
if err != nil {
return fmt.Errorf("failed to decode Key part: %v", err)
}

return uuid, nil
uuid[offset] = byte(n >> 24)
uuid[offset+1] = byte(n >> 16)
uuid[offset+2] = byte(n >> 8)
uuid[offset+3] = byte(n)
return nil
}

0 comments on commit 5e289c0

Please sign in to comment.