-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
x/crypto/cryptobyte: non allocating AddUint*LengthPrefixed and AddASN1 API #54059
x/crypto/cryptobyte: non allocating AddUint*LengthPrefixed and AddASN1 API #54059
Comments
CC @golang/security |
This seems like a good use case for a |
I am benchmarking now more AddUintLengthPrefixeds in single iteration.
BenchmarkAddLengthPrefixedOld (now uses sync.Pool)
(in this BenchmarkAddLengthPrefixedOld I also removed 2 additional allocations per AddUintLengthPrefixed) |
But we can add a new method called (maybe?) NoParentUsageCheck that will cause the AddUint*LengthPrefixed methods to return the original Builder, but then we would not get the parent builder usage panic (because we are reusing the original *Builder).
Edit: we might also just past nil as the *Builder in LengthPrefixed arg (after calling EmptyBuilder), this way we can remove this last 80B allocation. It looks like the heap analysis is smart, and it might remove the alloc.
This is how it could be used then:
|
This proposal has been added to the active column of the proposals project |
It would be good to find some way to do this optimization without doubling the API here. |
@rsc #54059 (comment) it will only introduce new method NoParentUsageCheck or EmptyBuilder. |
So to summary that, we have 2 possibilities:
b := NewBuilder(buf)
b.NoParentUsageCheck()
b.AddUint8LengthPrefixed(func(child *Builder) { {
child.AddBytes([]byte("123456"))
})
b := NewBuilder(buf)
b.EmptyBuilder()
b.AddUint8LengthPrefixed(func(_ *Builder) { {
b.AddBytes([]byte("123456"))
}) NoParentUsageCheck I feel like the EmptyBuilder way is more explicit what we are doing. NoParentUsageCheck might cause mistakes, which would be really confusing. (it will work, we cannot panic on parent usage, we are reusing *Builder): b := NewBuilder(buf)
b.NoParentUsageCheck()
b.AddUint8LengthPrefixed(func(c *Builder) { {
c.AddBytes([]byte("123456"))
b.AddBytes([]byte("123456"))
c.AddUint8LengthPrefixed(func(cc *Builder) { {
cc.AddBytes([]byte("123456"))
c.AddBytes([]byte("123456"))
})
}) It will still work as expected, AddBytes() will be written accordingly to the LengthPrefixed part in which in was executed. |
/cc @golang/security |
The b.NoParentUsageCheck() seems nice, but at that point why not make that the default, so that existing code all runs faster? I can't think of any case where it's valid to use c after the callback returns, and we know the callback never uses b (or cryptobyte would panic). So code should just run faster, with no API additions at all. Thoughts? |
I thought that we shouldn't get rid of this panic, that was indeed the reason why i proposed the new api. but as I think about it again it should be fine to do so. Overall I am fine with your idea, it will also simplify the implementation. |
@golang/security any thoughts about #54059 (comment)? That is, just start passing the parent Builder as the child Builder and let all code get faster. |
I am trying to figure out a Chesterton's Fence explanation for why the callbacks take a Builder in the first place, and why the panic is implemented and tested. If it's just because it's more explicit than requiring closures and to allow passing a Trying to use the parent Builder in the callback is not something developers might do with an expectation that it would do something else than write to the length-prefixed span, and Builders are not to be used concurrently. 👍 from me. |
Seems reasonable. This will require a not-insignificant change to the length prefixed logic to use a single builder rather than nested builders, but I think it makes for a slightly less confusing (and faster) API overall. |
@rsc does it have to be a proposal then? It will not introduce any new API. |
Change https://go.dev/cl/428475 mentions this issue: |
I prototyped an optimization in the CL (above). |
Re Chesterton, I think once you have child Builders vs parent Builders you have to have the panic to catch misuse due to confusion. But now what was formerly confused misuse would become correct code. Even though there are no new API functions, it's a significant enough semantic change to be worth continuing the proposal process to a resolution. |
Also I think that we should also add some strict rules to the docs of BuilderContinuation. Like: "inside |
Also it is worth noting that this will also affect AddASN1 method (not only the LengthPrefixed methods) |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
I may not have followed the discussion closely enough, but why is the sync.Pool not an option (as log does)? |
@andig using sync.Pool the performance was even worse than the original implementation. |
@mateusz834 please forgive me, responding as a learning exercise for me. Please don't consider me rude. I gave it a quick try as I happened to look into log's usage of sync.Pool time ago. Here's what I did:
In
plus
in the end. This is the benchmark difference:
Goes from
to
All tests pass except Maybe it's of any use... |
@andig
@andig Thanks for pointing this out. Not sure which method we should choose now. Apologies for the mistake. |
Change https://go.dev/cl/433503 mentions this issue: |
Imho one potential downside of the pool approach is that the |
The (*Builder).AddUint*LengthPrefixed allocates on every call a new *Builder.
This allocation cause ~17% of all heap allocations of a crypto/tls tls handshake.
Proposal:
Add new non allocating API Uint*LengthPrefixed:
The new methods will not return a *Builder in the func(), as the original API does.
Instedad it will require the usage of the original *Builder.
All writes to the original *Builder, inside Uint*LengthPrexied methods will
write them accordingly to the LengthPrefixed part in which they were executed.
Implementation example: commit
Benchmarks:
The text was updated successfully, but these errors were encountered: