Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blake2b: export Digest and (*Digest).Init #190

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

lukechampine
Copy link
Contributor

This allows clients to eliminate allocations when streaming data into the hash.

Thanks to golang/go#33160, allocations are already eliminated in many simple contexts. However, once you want to do anything more sophisticated -- specifically, introducing helper functions or types -- it becomes difficult or impossible to avoid allocations due to hash.Hash. As a motivating example:

// 1. This doesn't allocate, but also isn't very useful.
func hashUint64(u uint64) (sum [32]byte) {
    buf := make([]byte, 8)
    binary.LittleEndian.PutUint64(buf, u)
    h, _ := blake2b.New256(nil) // will be devirtualized to *blake2b.digest
    h.Write(buf)
    h.Sum(sum[:0])
    return
}

// 2. This allocates once per call.
func writeUint64(h hash.Hash, u uint64) {
    buf := make([]byte, 8)
    binary.LittleEndian.PutUint64(buf, u)
    h.Write(buf) // buf escapes here
}

// 3. This allocates twice per Uint64Hasher.
type Uint64Hasher struct {
    h   hash.Hash
    buf [8]byte
}
func (h *Uint64Hasher) WriteUint64(u uint64) {
    binary.LittleEndian.PutUint64(h.buf[:], u)
    h.h.Write(h.buf[:]) // h.buf escapes here
}
func NewUint64Hasher() *Uint64Hasher {
    h, _ := blake2b.New256(nil)
    return &Uint64Hasher{
        h: h, // h escapes here
    }
}

It is clear that substituting a concrete type for the hash.Hash in 2 and 3 will eliminate all allocations, but this is not currently possible. This PR exports blake2b.Digest, which allows clients to access the concrete type without breaking API compatibility.

Originally, I had hoped that merely exporting the type would be sufficient. Clients could then use a type assertion on the returned hash.Hash to convert it to a *blake2b.Digest. Unfortunately, this isn't good enough for NewUint64Hasher, because the combination of constructors invariably exceeds the inlining budget, causing the Digest to escape:

// cannot inline NewUint64Hasher: function too complex: cost 87 exceeds budget 80
func NewUint64Hasher() *Uint64Hasher {
    h, _ := blake2b.New256(nil)
    return &Uint64Hasher{
        h: h.(*blake2b.Digest),
    }
}

Now, it is possible to refactor blake2b.New256 such that the combined budget is not exceeded, by manually inlining newDigest and removing a few unnecessary expressions:

// can inline New256 with cost 61
func New256(key []byte) (hash.Hash, error) {
    if len(key) > Size {
        return nil, errKeySize
    }
    d := &Digest{
        size:   Size256,
        keyLen: len(key),
        h:      iv,
    }
    copy(d.key[:], key)
    d.h[0] ^= uint64(Size256) | (uint64(len(key)) << 8) | (1 << 16) | (1 << 24)
    if len(key) > 0 {
        d.block = d.key
        d.offset = BlockSize
    }
    return d, nil
}
// can inline NewUint64Hasher with cost 75

...but this is probably too ugly to be worth it, considering we'd have to do this for every constructor.

Another option would be to add a NewDigest(size int, key []byte) (*Digest, error) method. Unfortunately, this too exceeds the inlining budget, due to the errHashSize check.

So the only practical recourse is to add an Init method. We can then write the constructor as:

// can inline NewUint64Hasher with cost 74 
func NewUint64Hasher() *Uint64Hasher {
    h := new(blake2b.Digest)
    h.Init(blake2b.Size256, nil) // safe to ignore error
    return &Uint64Hasher{
        h: h,
    }
}

Since the Digest is declared in NewUint64Hasher, it won't escape to the heap as long as NewUint64Hasher itself can be inlined. Benchmarking confirms that this is zero-alloc:

func BenchmarkHasherAlloc(b *testing.B) {
    for i := 0; i < n; i++ {
        h := NewUint64Hasher()
        h.WriteUint64(uint64(i))
    }
}

Assuming this is accepted, I'm happy to open equivalent PRs for other hash functions as well.

@google-cla google-cla bot added the cla: yes label Aug 3, 2021
@gopherbot
Copy link
Contributor

This PR (HEAD: cb44cfb) has been imported to Gerrit for code review.

Please visit https://go-review.googlesource.com/c/crypto/+/339509 to see it.

Tip: You can toggle comments from me using the comments slash command (e.g. /comments off)
See the Wiki page for more info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants