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

Chore: Remove one allocate per hash by using generics. #5829

Merged
merged 3 commits into from
Nov 17, 2023

Conversation

jannotti
Copy link
Contributor

@jannotti jannotti commented Nov 13, 2023

The way we currently hash various objects is with:

// HashRep appends the correct hashid before the message to be hashed.
func HashRep(h Hashable) []byte {
	hashid, data := h.ToBeHashed()
	return append([]byte(hashid), data...)
}

This means that every callers generally have to allocate in order to convert their argument, which might be a BlockHeader, for example, into a Hashable. (This happens transparently, of course.)

However, by writing HashRep as:

func HashRep[H Hashable](h H) []byte {
	hashid, data := h.ToBeHashed()
	return append([]byte(hashid), data...)
}

We use generics to make a HashRep for each Hashable type. So a Hashable need not be created to call it. Thus we we get one fewer allocations most of the time.

As an example, BenchmarkGenesisHash gives:

BenchmarkGenesisHash/new-10         	 5553607	       213.5 ns/op	     256 B/op	       2 allocs/op
BenchmarkGenesisHash/old-10         	 4810734	       252.2 ns/op	     400 B/op	       3 allocs/op

One fewer alloc, and a small little speedup.

Summary

Test Plan

The way we currently hash various objects is with:

```
// HashRep appends the correct hashid before the message to be hashed.
func HashRep(h Hashable) []byte {
	hashid, data := h.ToBeHashed()
	return append([]byte(hashid), data...)
}
```

This means that every callers generally have to allocate in order to
convert their argument, which might be a `BlockHeader`, for example,
into a Hashable. (This happens transparently, of course.)

However, by writing HashRep as:

```
func HashRep[H Hashable](h H) []byte {
	hashid, data := h.ToBeHashed()
	return append([]byte(hashid), data...)
}
```

We use generics to make a HashRep for each Hashable type. So a
`Hashable` need not be created to call it. Thus we we get one fewer
allocations most of the time.

For this PR, I did this by writing `HashRepFast` instead, so that I
could commit some benchmarks.  They show one for allocation.

I had to create several copies of existsing functions to make the
Benchmarks work.  In the real PR, I'll remove all that extra stuff.
@jannotti jannotti changed the title Remove one allocate per hash by using generics. Chore: Remove one allocate per hash by using generics. Nov 13, 2023
@jannotti jannotti self-assigned this Nov 13, 2023
Copy link

codecov bot commented Nov 13, 2023

Codecov Report

Attention: 7 lines in your changes are missing coverage. Please review.

Comparison is base (1bb78de) 55.72% compared to head (25698af) 55.71%.
Report is 7 commits behind head on master.

Files Patch % Lines
cmd/goal/application.go 0.00% 4 Missing ⚠️
cmd/goal/interact.go 0.00% 2 Missing ⚠️
cmd/goal/tealsign.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5829      +/-   ##
==========================================
- Coverage   55.72%   55.71%   -0.01%     
==========================================
  Files         476      476              
  Lines       67131    67130       -1     
==========================================
- Hits        37408    37403       -5     
- Misses      27203    27204       +1     
- Partials     2520     2523       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jannotti jannotti marked this pull request as ready for review November 16, 2023 19:30
Copy link
Contributor

@algorandskiy algorandskiy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could post some benchmark results into the PR description?

crypto/util.go Outdated Show resolved Hide resolved
@jannotti
Copy link
Contributor Author

Could post some benchmark results into the PR description?

Added

BenchmarkGenesisHash/new-10 5553607 213.5 ns/op 256 B/op 2 allocs/op
BenchmarkGenesisHash/old-10 4810734 252.2 ns/op 400 B/op 3 allocs/op

Copy link
Contributor

@zeldovich zeldovich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good idea!

Another opportunity to save on allocations would be to call protocol.EncodeMsgp() instead of protocol.Encode() in ToBeHashed(). The extra allocation is coming from the check that protocol.Encode() is doing through CanMarshalMsg() to check whether obj directly implements msgp.Marshaler, or whether its msgp.Marshaler methods are promoted from some embedded struct field.

Alternatively, we could just decide that we don't have any more dangling embedded fields whose parent structs haven't gone through msgp. This used to happen when I was first incrementally adding support for msgp, but by now, everything has been msgp'ed already. So, we could change protocol.Encode() to drop that CanMarshalMsg() check and save an extra allocation.

@algorandskiy algorandskiy merged commit 06790fe into algorand:master Nov 17, 2023
PhearZero pushed a commit to PhearNet/crypto that referenced this pull request Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants