Skip to content

Commit 92f31ad

Browse files
committed
unroll loops when parsing base58
1 parent be654e9 commit 92f31ad

File tree

3 files changed

+160
-3
lines changed

3 files changed

+160
-3
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ This package is a fork of [github.com/gofrs/uuid](https://github.com/gofrs/uuid)
9090
This library includes additional performance optimizations beyond the original fork:
9191

9292
- **Zero allocations** for all parsing operations
93-
- **Optimized hex encoding/decoding** with lookup tables
93+
- **Optimized hex encoding/decoding** with lookup tables and unrolled loops
94+
- **Optimized base58 decoding** with stack allocation and loop unrolling (~29% faster)
9495

9596
## Benchmarks
9697

@@ -112,7 +113,7 @@ BenchmarkFromBytes 504783348 2.380 ns/op 0 B/op
112113
113114
BenchmarkFromString/canonical 153610305 7.834 ns/op 0 B/op 0 allocs/op
114115
BenchmarkFromString/hash 158399199 7.480 ns/op 0 B/op 0 allocs/op
115-
BenchmarkFromString/base58 16845720 71.05 ns/op 0 B/op 0 allocs/op
116+
BenchmarkFromString/base58 24494169 48.91 ns/op 0 B/op 0 allocs/op
116117
```
117118

118119
## Contributing

base58/base58.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func UnmarshalString(dst []byte, str string) error {
6969
return UnmarshalBytes(dst, []byte(str))
7070
}
7171

72-
func UnmarshalBytes(dst, src []byte) error {
72+
func UnmarshalBytesOld(dst, src []byte) error {
7373
outi := make([]uint32, 4) // (uuidSize + 3) / 4
7474

7575
for i := 0; i < len(src); i++ {
@@ -96,6 +96,91 @@ func UnmarshalBytes(dst, src []byte) error {
9696
return nil
9797
}
9898

99+
func UnmarshalBytes(dst, src []byte) error {
100+
// Use stack allocation for better performance
101+
var outi [4]uint32
102+
103+
// Optimized for the common case of 22-byte base58 UUID
104+
if len(src) == 22 {
105+
// Unrolled loop for base58 decoding
106+
// Process all 22 characters with partially unrolled loop
107+
var c uint64
108+
109+
// Unroll by 2 for better performance
110+
for i := 0; i < 22; i += 2 {
111+
// First character
112+
c = decode[src[i]]
113+
t3 := uint64(outi[3])*58 + c
114+
c = t3 >> 32
115+
outi[3] = uint32(t3)
116+
117+
t2 := uint64(outi[2])*58 + c
118+
c = t2 >> 32
119+
outi[2] = uint32(t2)
120+
121+
t1 := uint64(outi[1])*58 + c
122+
c = t1 >> 32
123+
outi[1] = uint32(t1)
124+
125+
t0 := uint64(outi[0])*58 + c
126+
outi[0] = uint32(t0)
127+
128+
// Second character (if exists)
129+
if i+1 < 22 {
130+
c = decode[src[i+1]]
131+
t3 = uint64(outi[3])*58 + c
132+
c = t3 >> 32
133+
outi[3] = uint32(t3)
134+
135+
t2 = uint64(outi[2])*58 + c
136+
c = t2 >> 32
137+
outi[2] = uint32(t2)
138+
139+
t1 = uint64(outi[1])*58 + c
140+
c = t1 >> 32
141+
outi[1] = uint32(t1)
142+
143+
t0 = uint64(outi[0])*58 + c
144+
outi[0] = uint32(t0)
145+
}
146+
}
147+
} else {
148+
// Fallback for non-standard lengths
149+
for i := 0; i < len(src); i++ {
150+
c := decode[src[i]]
151+
152+
for j := 3; j >= 0; j-- {
153+
t := uint64(outi[j])*58 + c
154+
c = t >> 32
155+
outi[j] = uint32(t)
156+
}
157+
}
158+
}
159+
160+
// Unrolled output conversion
161+
dst[0] = byte(outi[0] >> 24)
162+
dst[1] = byte(outi[0] >> 16)
163+
dst[2] = byte(outi[0] >> 8)
164+
dst[3] = byte(outi[0])
165+
166+
dst[4] = byte(outi[1] >> 24)
167+
dst[5] = byte(outi[1] >> 16)
168+
dst[6] = byte(outi[1] >> 8)
169+
dst[7] = byte(outi[1])
170+
171+
dst[8] = byte(outi[2] >> 24)
172+
dst[9] = byte(outi[2] >> 16)
173+
dst[10] = byte(outi[2] >> 8)
174+
dst[11] = byte(outi[2])
175+
176+
dst[12] = byte(outi[3] >> 24)
177+
dst[13] = byte(outi[3] >> 16)
178+
dst[14] = byte(outi[3] >> 8)
179+
dst[15] = byte(outi[3])
180+
181+
return nil
182+
}
183+
99184
func Encode(bin []byte) string {
100185
// A UUID will result in a base58 string of at most 22 characters.
101186
// This calculation is specific to 128-bit numbers (UUIDs).

base58/base58_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,74 @@ func BenchmarkDecode(b *testing.B) {
5858
Decode(testPairs[i].enc)
5959
}
6060
}
61+
62+
var testCases = []string{
63+
"1C9z3nFjeJ44HMBeuqGNxt",
64+
"6ba7b8109dad11d180b400c04f",
65+
"Xk7pWZaRRFkqbVa3ma7F5f",
66+
"11111111111111111111EJ",
67+
"zzzzzzzzzzzzzzzzzzzzzz",
68+
}
69+
70+
func BenchmarkUnmarshalBytesOld(b *testing.B) {
71+
dst := make([]byte, 16)
72+
src := []byte(testCases[0])
73+
74+
b.ResetTimer()
75+
for i := 0; i < b.N; i++ {
76+
_ = UnmarshalBytesOld(dst, src)
77+
}
78+
}
79+
80+
func BenchmarkUnmarshalBytesNew(b *testing.B) {
81+
dst := make([]byte, 16)
82+
src := []byte(testCases[0])
83+
84+
b.ResetTimer()
85+
for i := 0; i < b.N; i++ {
86+
_ = UnmarshalBytes(dst, src)
87+
}
88+
}
89+
90+
func BenchmarkUnmarshalBytesOldMultiple(b *testing.B) {
91+
dst := make([]byte, 16)
92+
93+
b.ResetTimer()
94+
for i := 0; i < b.N; i++ {
95+
for _, tc := range testCases {
96+
_ = UnmarshalBytesOld(dst, []byte(tc))
97+
}
98+
}
99+
}
100+
101+
func BenchmarkUnmarshalBytesNewMultiple(b *testing.B) {
102+
dst := make([]byte, 16)
103+
104+
b.ResetTimer()
105+
for i := 0; i < b.N; i++ {
106+
for _, tc := range testCases {
107+
_ = UnmarshalBytes(dst, []byte(tc))
108+
}
109+
}
110+
}
111+
112+
func TestUnmarshalBytesConsistency(t *testing.T) {
113+
for _, tc := range testCases {
114+
src := []byte(tc)
115+
dst1 := make([]byte, 16)
116+
dst2 := make([]byte, 16)
117+
118+
err1 := UnmarshalBytesOld(dst1, src)
119+
err2 := UnmarshalBytes(dst2, src)
120+
121+
if err1 != err2 {
122+
t.Fatalf("Error mismatch for %q: old=%v, new=%v", tc, err1, err2)
123+
}
124+
125+
for i := range dst1 {
126+
if dst1[i] != dst2[i] {
127+
t.Fatalf("Result mismatch for %q at byte %d: old=%x, new=%x", tc, i, dst1, dst2)
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)