Skip to content

Commit 41d0d7c

Browse files
authored
perf: faster hash marshaling methods (#183)
Reduce a bit of allocations and cpu time in Hash arshaling methods. Same behaviour as before but slightly in another way: instead of operations on string we can do this on []byte. As a result we now have tests for that. ``` go-header % go-perftuner bstat a.txt b.txt args: [a.txt b.txt]name old time/op new time/op delta HashMarshaling/String-10 798ns ± 0% 613ns ± 0% -23.08% (p=0.002 n=6+6) HashMarshaling/Marshal-10 1.11µs ± 0% 0.84µs ± 0% -24.55% (p=0.004 n=5+6) HashMarshaling/Unmarshal-10 333ns ±11% 277ns ± 1% -16.92% (p=0.004 n=5+6) name old alloc/op new alloc/op delta HashMarshaling/String-10 192B ± 0% 128B ± 0% -33.33% (p=0.002 n=6+6) HashMarshaling/Marshal-10 296B ± 0% 104B ± 0% -64.86% (p=0.002 n=6+6) HashMarshaling/Unmarshal-10 128B ± 0% 32B ± 0% -75.00% (p=0.002 n=6+6) name old allocs/op new allocs/op delta HashMarshaling/String-10 3.00 ± 0% 2.00 ± 0% ~ (p=0.002 n=6+6) HashMarshaling/Marshal-10 5.00 ± 0% 2.00 ± 0% -60.00% (p=0.002 n=6+6) HashMarshaling/Unmarshal-10 2.00 ± 0% 1.00 ± 0% ~ (p=0.002 n=6+6) ```
1 parent 672fc95 commit 41d0d7c

File tree

2 files changed

+99
-10
lines changed

2 files changed

+99
-10
lines changed

hash.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,51 @@ package header
33
import (
44
"encoding/hex"
55
"fmt"
6-
"strings"
76
)
87

98
// Hash represents cryptographic hash and provides basic serialization functions.
109
type Hash []byte
1110

1211
// String implements fmt.Stringer interface.
1312
func (h Hash) String() string {
14-
return strings.ToUpper(hex.EncodeToString(h))
13+
buf := make([]byte, hex.EncodedLen(len(h)))
14+
hex.Encode(buf, h)
15+
hexToUpper(buf)
16+
return string(buf)
1517
}
1618

1719
// MarshalJSON serializes Hash into valid JSON.
1820
func (h Hash) MarshalJSON() ([]byte, error) {
19-
s := strings.ToUpper(hex.EncodeToString(h))
20-
jbz := make([]byte, len(s)+2)
21-
jbz[0] = '"'
22-
copy(jbz[1:], s)
23-
jbz[len(jbz)-1] = '"'
24-
return jbz, nil
21+
buf := make([]byte, 2+hex.EncodedLen(len(h)))
22+
buf[0] = '"'
23+
hex.Encode(buf[1:], h)
24+
hexToUpper(buf)
25+
buf[len(buf)-1] = '"'
26+
return buf, nil
2527
}
2628

2729
// UnmarshalJSON deserializes JSON representation of a Hash into object.
2830
func (h *Hash) UnmarshalJSON(data []byte) error {
2931
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
3032
return fmt.Errorf("invalid hex string: %s", data)
3133
}
32-
bz2, err := hex.DecodeString(string(data[1 : len(data)-1]))
34+
35+
buf := make([]byte, hex.DecodedLen(len(data)-2))
36+
_, err := hex.Decode(buf, data[1:len(data)-1])
3337
if err != nil {
3438
return err
3539
}
36-
*h = bz2
40+
*h = buf
3741
return nil
3842
}
43+
44+
// because we encode hex (alphabet: 0-9a-f) we can do this inplace.
45+
func hexToUpper(b []byte) {
46+
for i := 0; i < len(b); i++ {
47+
c := b[i]
48+
if 'a' <= c && c <= 'z' {
49+
c -= 'a' - 'A'
50+
}
51+
b[i] = c
52+
}
53+
}

hash_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package header
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestHash(t *testing.T) {
13+
h := randHash()
14+
15+
buf, err := h.MarshalJSON()
16+
require.NoError(t, err)
17+
18+
var h2 Hash
19+
err = h2.UnmarshalJSON(buf)
20+
require.NoError(t, err)
21+
22+
require.Equal(t, h.String(), h2.String())
23+
}
24+
25+
func BenchmarkHashMarshaling(b *testing.B) {
26+
h := randHash()
27+
28+
golden, err := h.MarshalJSON()
29+
require.NoError(b, err)
30+
31+
b.ResetTimer()
32+
33+
b.Run("String", func(b *testing.B) {
34+
wantSize := hex.EncodedLen(len(h))
35+
36+
for i := 0; i < b.N; i++ {
37+
ln := len(h.String())
38+
require.Equal(b, ln, wantSize)
39+
}
40+
})
41+
42+
b.Run("Marshal", func(b *testing.B) {
43+
for i := 0; i < b.N; i++ {
44+
buf, err := h.MarshalJSON()
45+
require.NoError(b, err)
46+
require.NotZero(b, buf)
47+
}
48+
})
49+
50+
b.Run("Unmarshal", func(b *testing.B) {
51+
var h2 Hash
52+
53+
for i := 0; i < b.N; i++ {
54+
err := h2.UnmarshalJSON(golden)
55+
require.NoError(b, err)
56+
}
57+
})
58+
}
59+
60+
func Fuzz_hexToUpper(f *testing.F) {
61+
f.Add([]byte("48656c6c6f20476f7068657221"))
62+
63+
f.Fuzz(func(t *testing.T, buf []byte) {
64+
hexToUpper(buf)
65+
})
66+
}
67+
68+
func randHash() Hash {
69+
var buf [sha256.Size]byte
70+
if _, err := rand.Read(buf[:]); err != nil {
71+
panic(err)
72+
}
73+
return Hash(buf[:])
74+
}

0 commit comments

Comments
 (0)