From 14f83dbf84de7779a0af22bbde30e8590dc4fa32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Gr=C3=A4ger?= Date: Wed, 14 Feb 2024 11:22:29 +0100 Subject: [PATCH] Reduce String() allocations --- base32/base32.go | 27 ++++++++++++++++++++------- benchmark/benchmark_test.go | 3 +++ benchmark/go.mod | 10 ++++++---- benchmark/go.sum | 8 ++------ generate.go | 11 ++++++++++- random.go | 3 +++ sortable.go | 3 +++ 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/base32/base32.go b/base32/base32.go index 63b163d..3cbb9b7 100644 --- a/base32/base32.go +++ b/base32/base32.go @@ -34,13 +34,17 @@ func EncodeLower(src [16]byte) string { return Encode(src, alphLow) } -// Encode encodes the src [16]byte into a base32 string with the given alphabet. -// The alphabet must be 32 bytes long. If the alphabet is shorter, the function -// will panic. It's the callers responsiblity to ensure the alphabet is valid. -// -// Direct usage is discouraged. Use EncodeUpper or EncodeLower instead. -func Encode(src [16]byte, alphabet string) string { - dst := make([]byte, 26) +// EncodeLowerTo encodes the src [16]byte into a provided 26-byte buffer using uppercase letters. +func EncodeUpperTo(dst []byte, src [16]byte) { + EncodeTo(dst, src, alphUp) +} + +// EncodeLowerTo encodes the src [16]byte into a provided 26-byte buffer using lowercase letters. +func EncodeLowerTo(dst []byte, src [16]byte) { + EncodeTo(dst, src, alphLow) +} + +func EncodeTo(dst []byte, src [16]byte, alphabet string) { // Optimized unrolled loop ahead. // 10 byte timestamp @@ -72,7 +76,16 @@ func Encode(src [16]byte, alphabet string) string { dst[23] = alphabet[(src[14]&124)>>2] dst[24] = alphabet[((src[14]&3)<<3)|((src[15]&224)>>5)] dst[25] = alphabet[src[15]&31] +} +// Encode encodes the src [16]byte into a base32 string with the given alphabet. +// The alphabet must be 32 bytes long. If the alphabet is shorter, the function +// will panic. It's the callers responsiblity to ensure the alphabet is valid. +// +// Direct usage is discouraged. Use EncodeUpper or EncodeLower instead. +func Encode(src [16]byte, alphabet string) string { + dst := make([]byte, 26) + EncodeTo(dst, src, alphabet) return string(dst) } diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go index 41f7e4d..8f5cabd 100644 --- a/benchmark/benchmark_test.go +++ b/benchmark/benchmark_test.go @@ -86,6 +86,7 @@ func BenchmarkFromString(b *testing.B) { func benchStringRandom(n int) (string, func(*testing.B)) { ids := makeSortableIDs(n) return fmt.Sprintf("n=%d", n), func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { for idx := range ids { @@ -99,6 +100,7 @@ func benchStringRandom(n int) (string, func(*testing.B)) { func benchStringSortable(n int) (string, func(*testing.B)) { ids := makeSortableIDs(n) return fmt.Sprintf("n=%d", n), func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { for idx := range ids { @@ -112,6 +114,7 @@ func benchStringSortable(n int) (string, func(*testing.B)) { func benchStringJp(n int) (string, func(*testing.B)) { ids := makeJpTypeIDs(n) return fmt.Sprintf("n=%d", n), func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { for idx := range ids { diff --git a/benchmark/go.mod b/benchmark/go.mod index 653a373..9bffa4c 100644 --- a/benchmark/go.mod +++ b/benchmark/go.mod @@ -4,12 +4,14 @@ go 1.21.6 toolchain go1.21.7 -require github.com/sumup/typeid v0.0.0-20240207125954-757b87eaff3c +replace github.com/sumup/typeid => ../ + +require ( + github.com/sumup/typeid v0.0.0-20240207125954-757b87eaff3c + go.jetpack.io/typeid v1.0.0 +) require ( github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/jackc/pgx/v5 v5.5.3 // indirect - go.jetpack.io/typeid v1.0.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/sync v0.5.0 // indirect ) diff --git a/benchmark/go.sum b/benchmark/go.sum index a4a4a59..71f1eee 100644 --- a/benchmark/go.sum +++ b/benchmark/go.sum @@ -14,16 +14,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/sumup/typeid v0.0.0-20240207125954-757b87eaff3c h1:jHCDCEs+alb1DCheUJxR7IlXKr6Qn7cFUBVoZrxZoyQ= -github.com/sumup/typeid v0.0.0-20240207125954-757b87eaff3c/go.mod h1:gMQ6rvv9//+PumvkKu1tAOyXW/u9Pwg1dkbPWhQ62vk= -go.jetpack.io/typeid v0.1.0 h1:suTmjNR3y2em2gCTG06agFfcACm3+zuxfziMUk5UXnw= -go.jetpack.io/typeid v0.1.0/go.mod h1:E11ObFkKlvsSTxwwYIm+zpbsRthjIMDy/JGBogs2vSo= go.jetpack.io/typeid v1.0.0 h1:8gQ+iYGdyiQ0Pr40ydSB/PzMOIwlXX5DTojp1CBeSPQ= go.jetpack.io/typeid v1.0.0/go.mod h1:+UPEaECUgFxgAjFPn5Yf9eO/3ft/3xZ98Eahv9JW/GQ= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/generate.go b/generate.go index 75a85ac..1c20643 100644 --- a/generate.go +++ b/generate.go @@ -2,6 +2,7 @@ package typeid import ( "fmt" + "unsafe" "github.com/gofrs/uuid/v5" ) @@ -11,6 +12,8 @@ import ( type processor struct { // b32Encode applies a base32 encoding to a UUID. b32Encode func(uuid.UUID) string + // b32EncodeTo applies a base32 encoding to a UUID and copies the result into a provided 26-byte buffer. + b32EncodeTo func([]byte, uuid.UUID) // b32Decode decode a UUID using the resp. base32 decoding. b32Decode func(string) (uuid.UUID, error) // Generates a new universal unique identifer. @@ -89,5 +92,11 @@ func toString[P Prefix](suffix uuid.UUID, p *processor) string { if prefix == "" { return p.b32Encode(suffix) } - return prefix + "_" + p.b32Encode(suffix) + + buf := make([]byte, len(prefix)+1+suffixStrLen) + copy(buf, prefix) + copy(buf[len(prefix):], "_") + p.b32EncodeTo(buf[len(prefix)+1:], suffix) + + return unsafe.String(unsafe.SliceData(buf), len(buf)) } diff --git a/random.go b/random.go index c857185..18c75c9 100644 --- a/random.go +++ b/random.go @@ -16,6 +16,9 @@ var randomIDProc = &processor{ b32Encode: func(u uuid.UUID) string { return base32.EncodeUpper([16]byte(u)) }, + b32EncodeTo: func(dst []byte, u uuid.UUID) { + base32.EncodeUpperTo(dst, [16]byte(u)) + }, b32Decode: func(s string) (uuid.UUID, error) { decoded, err := base32.DecodeUpper(s) if err != nil { diff --git a/sortable.go b/sortable.go index 052d8a4..6809cc5 100644 --- a/sortable.go +++ b/sortable.go @@ -16,6 +16,9 @@ var sortableIDProc = &processor{ b32Encode: func(u uuid.UUID) string { return base32.EncodeLower([16]byte(u)) }, + b32EncodeTo: func(dst []byte, u uuid.UUID) { + base32.EncodeLowerTo(dst, [16]byte(u)) + }, b32Decode: func(s string) (uuid.UUID, error) { decoded, err := base32.DecodeLower(s) if err != nil {