Skip to content

Commit 97240d5

Browse files
magicalFiloSottile
authored andcommitted
crypto/hmac: speed up repeated operations with the same key
Speed up repeated HMAC operations with the same key by not recomputing the first block of the inner and outer hashes in Reset and Sum, saving two block computations each time. This is a significant win for applications which hash many small messages with the same key. In x/crypto/pbkdf2 for example, this optimization cuts the number of block computations in half, speeding it up by 25%-40% depending on the hash function. The hash function needs to implement binary.Marshaler and binary.Unmarshaler for this optimization to work, so that we can save and restore its internal state. All hash functions in the standard library are marshalable (CL 66710) but if the hash isn't marshalable, we fall back on the old behaviour. Marshaling the hashes does add a couple unavoidable new allocations, but this only has to be done once, so the cost is amortized over repeated uses. To minimize impact to applications which don't (or can't) reuse hmac objects, marshaling is performed in Reset (rather than in New), since calling Reset seems like a good indication that the caller intends to reuse the hmac object later. I had to add a boolean field to the hmac state to remember if we've marshaled the hashes or not. This is paid for by removing the size and blocksize fields, which were basically unused except for some initialization work in New, and to fulfill the Size and Blocksize methods. Size and Blocksize can just be forwarded to the underlying hash, so there doesn't really seem to be any reason to waste space caching their values. crypto/hmac benchmarks: name old time/op new time/op delta HMAC_Reset/SHA1/1K-2 4.06µs ± 0% 3.77µs ± 0% -7.29% (p=0.000 n=8+10) HMAC_Reset/SHA1/32-2 1.08µs ± 0% 0.78µs ± 1% -27.67% (p=0.000 n=10+10) HMAC_Reset/SHA256/1K-2 10.3µs ± 0% 9.4µs ± 0% -9.03% (p=0.000 n=10+10) HMAC_Reset/SHA256/32-2 2.32µs ± 0% 1.42µs ± 0% -38.87% (p=0.000 n=10+10) HMAC_Reset/SHA512/1K-2 8.22µs ± 0% 7.04µs ± 0% -14.32% (p=0.000 n=9+9) HMAC_Reset/SHA512/32-2 3.08µs ± 0% 1.89µs ± 0% -38.54% (p=0.000 n=10+9) HMAC_New/SHA1/1K-2 4.86µs ± 1% 4.93µs ± 1% +1.30% (p=0.000 n=10+9) HMAC_New/SHA1/32-2 1.91µs ± 1% 1.95µs ± 1% +1.84% (p=0.000 n=10+9) HMAC_New/SHA256/1K-2 11.2µs ± 1% 11.2µs ± 0% ~ (p=1.000 n=9+10) HMAC_New/SHA256/32-2 3.22µs ± 2% 3.19µs ± 2% -1.07% (p=0.018 n=9+10) HMAC_New/SHA512/1K-2 9.54µs ± 0% 9.66µs ± 1% +1.31% (p=0.000 n=9+10) HMAC_New/SHA512/32-2 4.37µs ± 1% 4.46µs ± 1% +1.97% (p=0.000 n=10+9) name old speed new speed delta HMAC_Reset/SHA1/1K-2 252MB/s ± 0% 272MB/s ± 0% +7.86% (p=0.000 n=8+10) HMAC_Reset/SHA1/32-2 29.7MB/s ± 0% 41.1MB/s ± 1% +38.26% (p=0.000 n=10+10) HMAC_Reset/SHA256/1K-2 99.1MB/s ± 0% 108.9MB/s ± 0% +9.93% (p=0.000 n=10+10) HMAC_Reset/SHA256/32-2 13.8MB/s ± 0% 22.6MB/s ± 0% +63.57% (p=0.000 n=10+10) HMAC_Reset/SHA512/1K-2 125MB/s ± 0% 145MB/s ± 0% +16.71% (p=0.000 n=9+9) HMAC_Reset/SHA512/32-2 10.4MB/s ± 0% 16.9MB/s ± 0% +62.69% (p=0.000 n=10+9) HMAC_New/SHA1/1K-2 211MB/s ± 1% 208MB/s ± 1% -1.29% (p=0.000 n=10+9) HMAC_New/SHA1/32-2 16.7MB/s ± 1% 16.4MB/s ± 1% -1.81% (p=0.000 n=10+9) HMAC_New/SHA256/1K-2 91.3MB/s ± 1% 91.5MB/s ± 0% ~ (p=0.950 n=9+10) HMAC_New/SHA256/32-2 9.94MB/s ± 2% 10.04MB/s ± 2% +1.09% (p=0.021 n=9+10) HMAC_New/SHA512/1K-2 107MB/s ± 0% 106MB/s ± 1% -1.29% (p=0.000 n=9+10) HMAC_New/SHA512/32-2 7.32MB/s ± 1% 7.18MB/s ± 1% -1.89% (p=0.000 n=10+9) name old alloc/op new alloc/op delta HMAC_Reset/SHA1/1K-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_Reset/SHA1/32-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_Reset/SHA256/1K-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_Reset/SHA256/32-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_Reset/SHA512/1K-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_Reset/SHA512/32-2 0.00B ±NaN% 0.00B ±NaN% ~ (all samples are equal) HMAC_New/SHA1/1K-2 448B ± 0% 448B ± 0% ~ (all samples are equal) HMAC_New/SHA1/32-2 448B ± 0% 448B ± 0% ~ (all samples are equal) HMAC_New/SHA256/1K-2 480B ± 0% 480B ± 0% ~ (all samples are equal) HMAC_New/SHA256/32-2 480B ± 0% 480B ± 0% ~ (all samples are equal) HMAC_New/SHA512/1K-2 800B ± 0% 800B ± 0% ~ (all samples are equal) HMAC_New/SHA512/32-2 800B ± 0% 800B ± 0% ~ (all samples are equal) name old allocs/op new allocs/op delta HMAC_Reset/SHA1/1K-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_Reset/SHA1/32-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_Reset/SHA256/1K-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_Reset/SHA256/32-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_Reset/SHA512/1K-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_Reset/SHA512/32-2 0.00 ±NaN% 0.00 ±NaN% ~ (all samples are equal) HMAC_New/SHA1/1K-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) HMAC_New/SHA1/32-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) HMAC_New/SHA256/1K-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) HMAC_New/SHA256/32-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) HMAC_New/SHA512/1K-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) HMAC_New/SHA512/32-2 5.00 ± 0% 5.00 ± 0% ~ (all samples are equal) x/crypto/pbkdf2 benchmarks: name old time/op new time/op delta HMACSHA1-2 4.63ms ± 0% 3.40ms ± 0% -26.58% (p=0.000 n=10+9) HMACSHA256-2 9.75ms ± 0% 5.98ms ± 0% -38.62% (p=0.000 n=9+10) name old alloc/op new alloc/op delta HMACSHA1-2 516B ± 0% 708B ± 0% +37.21% (p=0.000 n=10+10) HMACSHA256-2 549B ± 0% 772B ± 0% +40.62% (p=0.000 n=10+10) name old allocs/op new allocs/op delta HMACSHA1-2 8.00 ± 0% 10.00 ± 0% +25.00% (p=0.000 n=10+10) HMACSHA256-2 8.00 ± 0% 10.00 ± 0% +25.00% (p=0.000 n=10+10) Fixes #19941 Change-Id: I7077a6f875be68d3da05f7b3664e18514861886f Reviewed-on: https://go-review.googlesource.com/c/go/+/27458 Run-TryBot: Emmanuel Odeke <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent cb14bd8 commit 97240d5

File tree

2 files changed

+80
-13
lines changed

2 files changed

+80
-13
lines changed

src/crypto/hmac/hmac.go

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,36 @@ import (
3434
// opad = 0x5c byte repeated for key length
3535
// hmac = H([key ^ opad] H([key ^ ipad] text))
3636

37+
// Marshalable is the combination of encoding.BinaryMarshaler and
38+
// encoding.BinaryUnmarshaler. Their method definitions are repeated here to
39+
// avoid a dependency on the encoding package.
40+
type marshalable interface {
41+
MarshalBinary() ([]byte, error)
42+
UnmarshalBinary([]byte) error
43+
}
44+
3745
type hmac struct {
38-
size int
39-
blocksize int
4046
opad, ipad []byte
4147
outer, inner hash.Hash
48+
49+
// If marshaled is true, then opad and ipad do not contain a padded
50+
// copy of the key, but rather the marshaled state of outer/inner after
51+
// opad/ipad has been fed into it.
52+
marshaled bool
4253
}
4354

4455
func (h *hmac) Sum(in []byte) []byte {
4556
origLen := len(in)
4657
in = h.inner.Sum(in)
47-
h.outer.Reset()
48-
h.outer.Write(h.opad)
58+
59+
if h.marshaled {
60+
if err := h.outer.(marshalable).UnmarshalBinary(h.opad); err != nil {
61+
panic(err)
62+
}
63+
} else {
64+
h.outer.Reset()
65+
h.outer.Write(h.opad)
66+
}
4967
h.outer.Write(in[origLen:])
5068
return h.outer.Sum(in[:origLen])
5169
}
@@ -54,13 +72,51 @@ func (h *hmac) Write(p []byte) (n int, err error) {
5472
return h.inner.Write(p)
5573
}
5674

57-
func (h *hmac) Size() int { return h.size }
58-
59-
func (h *hmac) BlockSize() int { return h.blocksize }
75+
func (h *hmac) Size() int { return h.outer.Size() }
76+
func (h *hmac) BlockSize() int { return h.inner.BlockSize() }
6077

6178
func (h *hmac) Reset() {
79+
if h.marshaled {
80+
if err := h.inner.(marshalable).UnmarshalBinary(h.ipad); err != nil {
81+
panic(err)
82+
}
83+
return
84+
}
85+
6286
h.inner.Reset()
6387
h.inner.Write(h.ipad)
88+
89+
// If the underlying hash is marshalable, we can save some time by
90+
// saving a copy of the hash state now, and restoring it on future
91+
// calls to Reset and Sum instead of writing ipad/opad every time.
92+
//
93+
// If either hash is unmarshalable for whatever reason,
94+
// it's safe to bail out here.
95+
marshalableInner, innerOK := h.inner.(marshalable)
96+
if !innerOK {
97+
return
98+
}
99+
marshalableOuter, outerOK := h.outer.(marshalable)
100+
if !outerOK {
101+
return
102+
}
103+
104+
imarshal, err := marshalableInner.MarshalBinary()
105+
if err != nil {
106+
return
107+
}
108+
109+
h.outer.Reset()
110+
h.outer.Write(h.opad)
111+
omarshal, err := marshalableOuter.MarshalBinary()
112+
if err != nil {
113+
return
114+
}
115+
116+
// Marshaling succeeded; save the marshaled state for later
117+
h.ipad = imarshal
118+
h.opad = omarshal
119+
h.marshaled = true
64120
}
65121

66122
// New returns a new HMAC hash using the given hash.Hash type and key.
@@ -71,11 +127,10 @@ func New(h func() hash.Hash, key []byte) hash.Hash {
71127
hm := new(hmac)
72128
hm.outer = h()
73129
hm.inner = h()
74-
hm.size = hm.inner.Size()
75-
hm.blocksize = hm.inner.BlockSize()
76-
hm.ipad = make([]byte, hm.blocksize)
77-
hm.opad = make([]byte, hm.blocksize)
78-
if len(key) > hm.blocksize {
130+
blocksize := hm.inner.BlockSize()
131+
hm.ipad = make([]byte, blocksize)
132+
hm.opad = make([]byte, blocksize)
133+
if len(key) > blocksize {
79134
// If key is too big, hash it.
80135
hm.outer.Write(key)
81136
key = hm.outer.Sum(nil)
@@ -89,6 +144,7 @@ func New(h func() hash.Hash, key []byte) hash.Hash {
89144
hm.opad[i] ^= 0x5c
90145
}
91146
hm.inner.Write(hm.ipad)
147+
92148
return hm
93149
}
94150

src/crypto/hmac/hmac_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ func TestHMAC(t *testing.T) {
529529
if b := h.BlockSize(); b != tt.blocksize {
530530
t.Errorf("BlockSize: got %v, want %v", b, tt.blocksize)
531531
}
532-
for j := 0; j < 2; j++ {
532+
for j := 0; j < 4; j++ {
533533
n, err := h.Write(tt.in)
534534
if n != len(tt.in) || err != nil {
535535
t.Errorf("test %d.%d: Write(%d) = %d, %v", i, j, len(tt.in), n, err)
@@ -546,10 +546,21 @@ func TestHMAC(t *testing.T) {
546546

547547
// Second iteration: make sure reset works.
548548
h.Reset()
549+
550+
// Third and fourth iteration: make sure hmac works on
551+
// hashes without MarshalBinary/UnmarshalBinary
552+
if j == 1 {
553+
h = New(func() hash.Hash { return justHash{tt.hash()} }, tt.key)
554+
}
549555
}
550556
}
551557
}
552558

559+
// justHash implements just the hash.Hash methods and nothing else
560+
type justHash struct {
561+
hash.Hash
562+
}
563+
553564
func TestEqual(t *testing.T) {
554565
a := []byte("test")
555566
b := []byte("test1")

0 commit comments

Comments
 (0)