From 871c443366498bd7003dc45453985f19fa170bd8 Mon Sep 17 00:00:00 2001 From: Anatoly Kussul Date: Thu, 28 Nov 2024 03:17:26 +0100 Subject: [PATCH 1/5] some optimizations --- base57.go | 89 +++++++++++++++++++++++++++--------------------------- uint128.go | 45 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 uint128.go diff --git a/base57.go b/base57.go index 2499600..5182ce2 100644 --- a/base57.go +++ b/base57.go @@ -1,11 +1,7 @@ package shortuuid import ( - "fmt" - "math" - "math/big" - "strings" - + "encoding/binary" "github.com/google/uuid" ) @@ -14,46 +10,43 @@ type base57 struct { alphabet alphabet } +const strLen = 22 + // Encode encodes uuid.UUID into a string using the most significant bits (MSB) // first according to the alphabet. func (b base57) Encode(u uuid.UUID) string { - var num big.Int - num.SetString(strings.Replace(u.String(), "-", "", 4), 16) - - // Calculate encoded length. - length := math.Ceil(math.Log(math.Pow(2, 128)) / math.Log(float64(b.alphabet.Length()))) + num := uint128{ + binary.BigEndian.Uint64(u[8:]), + binary.BigEndian.Uint64(u[:8]), + } - return b.numToString(&num, int(length)) + return b.numToString(num) } // Decode decodes a string according to the alphabet into a uuid.UUID. If s is // too short, its most significant bits (MSB) will be padded with 0 (zero). func (b base57) Decode(u string) (uuid.UUID, error) { - str, err := b.stringToNum(u) + buf, err := b.stringToNumBytes(u) if err != nil { return uuid.Nil, err } - return uuid.Parse(str) + return uuid.FromBytes(buf) } // numToString converts a number a string using the given alphabet. -func (b *base57) numToString(number *big.Int, padToLen int) string { - var ( - out []rune - digit *big.Int - ) - - alphaLen := big.NewInt(b.alphabet.Length()) - - zero := new(big.Int) - for number.Cmp(zero) > 0 { - number, digit = new(big.Int).DivMod(number, alphaLen, new(big.Int)) - out = append(out, b.alphabet.chars[digit.Int64()]) +func (b *base57) numToString(number uint128) string { + var digit uint64 + out := make([]rune, strLen) + + i := 0 + for number.Hi > 0 || number.Lo > 0 { + number, digit = number.quoRem64(uint64(b.alphabet.Length())) + out[i] = b.alphabet.chars[digit] + i++ } - - if padToLen > 0 { - remainder := math.Max(float64(padToLen-len(out)), 0) - out = append(out, []rune(strings.Repeat(string(b.alphabet.chars[0]), int(remainder)))...) + for i < strLen { + out[i] = b.alphabet.chars[0] + i++ } reverse(out) @@ -61,31 +54,39 @@ func (b *base57) numToString(number *big.Int, padToLen int) string { return string(out) } -// stringToNum converts a string a number using the given alphabet. -func (b *base57) stringToNum(s string) (string, error) { - n := big.NewInt(0) +// stringToNumBytes converts a string a number using the given alphabet. +func (b *base57) stringToNumBytes(s string) ([]byte, error) { + var ( + n uint128 + err error + index int64 + ) for _, char := range s { - n.Mul(n, big.NewInt(b.alphabet.Length())) - - index, err := b.alphabet.Index(char) + n, err = n.mul64(uint64(b.alphabet.Length())) if err != nil { - return "", err + return nil, err } - n.Add(n, big.NewInt(index)) - } + index, err = b.alphabet.Index(char) + if err != nil { + return nil, err + } - if n.BitLen() > 128 { - return "", fmt.Errorf("number is out of range (need a 128-bit value)") + n, err = n.add64(uint64(index)) + if err != nil { + return nil, err + } } - - return fmt.Sprintf("%032x", n), nil + buf := make([]byte, 16) + n.putBytes(buf) + return buf, nil } // reverse reverses a inline. func reverse(a []rune) { - for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { - a[i], a[j] = a[j], a[i] + n := len(a) + for i := 0; i < n/2; i++ { + a[i], a[n-1-i] = a[n-1-i], a[i] } } diff --git a/uint128.go b/uint128.go new file mode 100644 index 0000000..5c233eb --- /dev/null +++ b/uint128.go @@ -0,0 +1,45 @@ +package shortuuid + +import ( + "encoding/binary" + "fmt" + "math/bits" +) + +type uint128 struct { + Lo, Hi uint64 +} + +func (u uint128) quoRem64(v uint64) (q uint128, r uint64) { + if u.Hi < v { + q.Lo, r = bits.Div64(u.Hi, u.Lo, v) + } else { + q.Hi, r = bits.Div64(0, u.Hi, v) + q.Lo, r = bits.Div64(r, u.Lo, v) + } + return +} + +func (u uint128) mul64(v uint64) (uint128, error) { + hi, lo := bits.Mul64(u.Lo, v) + p0, p1 := bits.Mul64(u.Hi, v) + hi, c0 := bits.Add64(hi, p1, 0) + if p0 != 0 || c0 != 0 { + return uint128{}, fmt.Errorf("number is out of range (need a 128-bit value)") + } + return uint128{lo, hi}, nil +} + +func (u uint128) add64(v uint64) (uint128, error) { + lo, carry := bits.Add64(u.Lo, v, 0) + hi, carry := bits.Add64(u.Hi, 0, carry) + if carry != 0 { + return uint128{}, fmt.Errorf("number is out of range (need a 128-bit value)") + } + return uint128{lo, hi}, nil +} + +func (u uint128) putBytes(b []byte) { + binary.BigEndian.PutUint64(b[:8], u.Hi) + binary.BigEndian.PutUint64(b[8:], u.Lo) +} From f8cd3df35a16a6f879c23d40e3fa80dcfb86bf21 Mon Sep 17 00:00:00 2001 From: Anatoly Kussul Date: Thu, 28 Nov 2024 05:07:29 +0100 Subject: [PATCH 2/5] move alphabet len to const --- base57.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/base57.go b/base57.go index 5182ce2..f81b515 100644 --- a/base57.go +++ b/base57.go @@ -10,7 +10,10 @@ type base57 struct { alphabet alphabet } -const strLen = 22 +const ( + strLen = 22 + alphabetLen = 57 +) // Encode encodes uuid.UUID into a string using the most significant bits (MSB) // first according to the alphabet. @@ -40,7 +43,7 @@ func (b *base57) numToString(number uint128) string { i := 0 for number.Hi > 0 || number.Lo > 0 { - number, digit = number.quoRem64(uint64(b.alphabet.Length())) + number, digit = number.quoRem64(alphabetLen) out[i] = b.alphabet.chars[digit] i++ } From 61b443094e2d6a607e4bdd8dd83f2184a2c928ea Mon Sep 17 00:00:00 2001 From: Anatoly Kussul Date: Thu, 28 Nov 2024 05:59:44 +0100 Subject: [PATCH 3/5] use alphabet const in decode --- base57.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base57.go b/base57.go index f81b515..c8c2e84 100644 --- a/base57.go +++ b/base57.go @@ -66,7 +66,7 @@ func (b *base57) stringToNumBytes(s string) ([]byte, error) { ) for _, char := range s { - n, err = n.mul64(uint64(b.alphabet.Length())) + n, err = n.mul64(alphabetLen) if err != nil { return nil, err } From 17c915d5bdf04900619686625b2d0c25f5de268b Mon Sep 17 00:00:00 2001 From: Anatoly Kussul Date: Thu, 28 Nov 2024 06:22:09 +0100 Subject: [PATCH 4/5] remove the need for reverse --- base57.go | 18 ++++-------------- base57_test.go | 13 ------------- 2 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 base57_test.go diff --git a/base57.go b/base57.go index c8c2e84..babdb5d 100644 --- a/base57.go +++ b/base57.go @@ -41,19 +41,17 @@ func (b *base57) numToString(number uint128) string { var digit uint64 out := make([]rune, strLen) - i := 0 + i := strLen - 1 for number.Hi > 0 || number.Lo > 0 { number, digit = number.quoRem64(alphabetLen) out[i] = b.alphabet.chars[digit] - i++ + i-- } - for i < strLen { + for i >= 0 { out[i] = b.alphabet.chars[0] - i++ + i-- } - reverse(out) - return string(out) } @@ -85,11 +83,3 @@ func (b *base57) stringToNumBytes(s string) ([]byte, error) { n.putBytes(buf) return buf, nil } - -// reverse reverses a inline. -func reverse(a []rune) { - n := len(a) - for i := 0; i < n/2; i++ { - a[i], a[n-1-i] = a[n-1-i], a[i] - } -} diff --git a/base57_test.go b/base57_test.go deleted file mode 100644 index 0a1f2ac..0000000 --- a/base57_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package shortuuid - -import ( - "testing" -) - -func TestReverse(t *testing.T) { - a := []rune("abc123") - reverse(a) - if string(a) != "321cba" { - t.Errorf("expected string to be %q, got %q", "321cba", string(a)) - } -} From 3cea18cd61ce8ea4b46349f56fdcb1045cda6fdb Mon Sep 17 00:00:00 2001 From: Anatoly Kussul Date: Fri, 29 Nov 2024 05:19:07 +0100 Subject: [PATCH 5/5] use strings.Builder, instead of directly stringifying []runes, some more refactoring and simplifications --- base57.go | 89 +++++++++++++++++++++++------------------------ shortuuid_test.go | 2 +- uint128.go | 45 ------------------------ 3 files changed, 44 insertions(+), 92 deletions(-) delete mode 100644 uint128.go diff --git a/base57.go b/base57.go index babdb5d..6fe7482 100644 --- a/base57.go +++ b/base57.go @@ -2,7 +2,10 @@ package shortuuid import ( "encoding/binary" + "fmt" "github.com/google/uuid" + "math/bits" + "strings" ) type base57 struct { @@ -22,64 +25,58 @@ func (b base57) Encode(u uuid.UUID) string { binary.BigEndian.Uint64(u[8:]), binary.BigEndian.Uint64(u[:8]), } + var outIndexes [strLen]uint64 - return b.numToString(num) -} - -// Decode decodes a string according to the alphabet into a uuid.UUID. If s is -// too short, its most significant bits (MSB) will be padded with 0 (zero). -func (b base57) Decode(u string) (uuid.UUID, error) { - buf, err := b.stringToNumBytes(u) - if err != nil { - return uuid.Nil, err + for i := strLen - 1; num.Hi > 0 || num.Lo > 0; i-- { + num, outIndexes[i] = num.quoRem64(alphabetLen) } - return uuid.FromBytes(buf) -} - -// numToString converts a number a string using the given alphabet. -func (b *base57) numToString(number uint128) string { - var digit uint64 - out := make([]rune, strLen) - i := strLen - 1 - for number.Hi > 0 || number.Lo > 0 { - number, digit = number.quoRem64(alphabetLen) - out[i] = b.alphabet.chars[digit] - i-- + var sb strings.Builder + sb.Grow(strLen) + for i := 0; i < strLen; i++ { + sb.WriteRune(b.alphabet.chars[outIndexes[i]]) } - for i >= 0 { - out[i] = b.alphabet.chars[0] - i-- - } - - return string(out) + return sb.String() } -// stringToNumBytes converts a string a number using the given alphabet. -func (b *base57) stringToNumBytes(s string) ([]byte, error) { - var ( - n uint128 - err error - index int64 - ) +// Decode decodes a string according to the alphabet into a uuid.UUID. If s is +// too short, its most significant bits (MSB) will be padded with 0 (zero). +func (b base57) Decode(s string) (u uuid.UUID, err error) { + var n uint128 + var index int64 for _, char := range s { - n, err = n.mul64(alphabetLen) - if err != nil { - return nil, err - } - index, err = b.alphabet.Index(char) if err != nil { - return nil, err + return } - - n, err = n.add64(uint64(index)) + n, err = n.mulAdd64(alphabetLen, uint64(index)) if err != nil { - return nil, err + return } } - buf := make([]byte, 16) - n.putBytes(buf) - return buf, nil + binary.BigEndian.PutUint64(u[:8], n.Hi) + binary.BigEndian.PutUint64(u[8:], n.Lo) + return +} + +type uint128 struct { + Lo, Hi uint64 +} + +func (u uint128) quoRem64(v uint64) (q uint128, r uint64) { + q.Hi, r = bits.Div64(0, u.Hi, v) + q.Lo, r = bits.Div64(r, u.Lo, v) + return +} + +func (u uint128) mulAdd64(m uint64, a uint64) (uint128, error) { + hi, lo := bits.Mul64(u.Lo, m) + p0, p1 := bits.Mul64(u.Hi, m) + lo, c0 := bits.Add64(lo, a, 0) + hi, c1 := bits.Add64(hi, p1, c0) + if p0 != 0 || c1 != 0 { + return uint128{}, fmt.Errorf("number is out of range (need a 128-bit value)") + } + return uint128{lo, hi}, nil } diff --git a/shortuuid_test.go b/shortuuid_test.go index 1bc6492..d5f0b71 100644 --- a/shortuuid_test.go +++ b/shortuuid_test.go @@ -224,6 +224,6 @@ func BenchmarkEncoding(b *testing.B) { func BenchmarkDecoding(b *testing.B) { for i := 0; i < b.N; i++ { - _, _ = DefaultEncoder.Decode("c3eeb3e6-e577-4de2-b5bb-08371196b453") + _, _ = DefaultEncoder.Decode("nUfojcH2M5j9j3Tk5A8mf7") } } diff --git a/uint128.go b/uint128.go deleted file mode 100644 index 5c233eb..0000000 --- a/uint128.go +++ /dev/null @@ -1,45 +0,0 @@ -package shortuuid - -import ( - "encoding/binary" - "fmt" - "math/bits" -) - -type uint128 struct { - Lo, Hi uint64 -} - -func (u uint128) quoRem64(v uint64) (q uint128, r uint64) { - if u.Hi < v { - q.Lo, r = bits.Div64(u.Hi, u.Lo, v) - } else { - q.Hi, r = bits.Div64(0, u.Hi, v) - q.Lo, r = bits.Div64(r, u.Lo, v) - } - return -} - -func (u uint128) mul64(v uint64) (uint128, error) { - hi, lo := bits.Mul64(u.Lo, v) - p0, p1 := bits.Mul64(u.Hi, v) - hi, c0 := bits.Add64(hi, p1, 0) - if p0 != 0 || c0 != 0 { - return uint128{}, fmt.Errorf("number is out of range (need a 128-bit value)") - } - return uint128{lo, hi}, nil -} - -func (u uint128) add64(v uint64) (uint128, error) { - lo, carry := bits.Add64(u.Lo, v, 0) - hi, carry := bits.Add64(u.Hi, 0, carry) - if carry != 0 { - return uint128{}, fmt.Errorf("number is out of range (need a 128-bit value)") - } - return uint128{lo, hi}, nil -} - -func (u uint128) putBytes(b []byte) { - binary.BigEndian.PutUint64(b[:8], u.Hi) - binary.BigEndian.PutUint64(b[8:], u.Lo) -}