-
Notifications
You must be signed in to change notification settings - Fork 4.6k
transport: Reduce pointer usage in Stream structs #8624
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
base: master
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8624 +/- ##
==========================================
- Coverage 82.12% 82.01% -0.12%
==========================================
Files 415 415
Lines 40701 40709 +8
==========================================
- Hits 33425 33386 -39
- Misses 5895 5937 +42
- Partials 1381 1386 +5
🚀 New features to boost your workflow:
|
a44546b
to
4ebd663
Compare
4ebd663
to
42b1067
Compare
internal/transport/flowcontrol.go
Outdated
func newWriteQuota(sz int32, done <-chan struct{}) *writeQuota { | ||
w := &writeQuota{ | ||
func initWriteQuota(wq *writeQuota, sz int32, done <-chan struct{}) { | ||
*wq = writeQuota{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syntax does feel a little weird to me. Did you try some of these options to see if they read better (and don't perform worse)?
- directly set fields of
wq
in here instead of settingwq
to a completely new instance ofwriteQuota
- Can this
initWriteQuota
be a method onStream
? - Can we make the zero value of
writeQouta
something that can actually work?
This comment applies to other types as well like recvBuffer
.
Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I refactored the function into a method with a pointer receiver and replaced the single struct literal assignment with individual field assignments. I also added a godoc comment to explain that this initialization pattern is used to avoid heap allocations. Individual field assignment may be slightly faster than the struct assignment as it avoid allocating an intermediate object on the stack before copying it over.
I defined the method on the writeQuota
struct itself, rather than on the Stream
struct. This keeps the initialization logic co-located with its type and reduces coupling with Stream
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we ensure that we don't have pointer fields inside structs (as much as possible/feasible/required) going forward? At least personally, if I have a field in a struct that has a pointer receiver, I by default store that field as a pointer. And if I see someone else doing the same in a code review, I would not even notice it, because it is so ingrained in me.
Some of the things mentioned in the docs you said were very useful. But I'm wondering how we make that knowledge more accessible to everyone on the team and ensure we keep certain things in mind when writing and reviewing code.
bytesReceived atomic.Bool // indicates whether any bytes have been received on this stream | ||
unprocessed atomic.Bool // set if the server sends a refused stream or GOAWAY including this stream | ||
|
||
status *status.Status // the status error received from the server |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we move this to the top as well as the link you sent initially also said grouping pointer fields at the top of the struct helps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's any way to make this foolproof. We must allow pointers in structs, for obvious reasons. We could possibly add a test that runs a quick local benchmark for a fixed number of iterations, and checks the number & size of allocations afterwards?
The pprof profiles for unary RPC benchmarks indicate significant time spent in
runtime.mallocgc
andruntime.gcBgMarkWorker
. This indicates gRPC is spending significant CPU cycles allocating or garbage collecting.This change reduces the number of pointer fields in the structs that represent client and server stream. This will reduce number of memory allocations (faster) and also reduce pressure on garbage collector (faster garbage collections) since the GC doesn't need to scan non-pointer fields. For structs which were stored as pointers to ensure values are not copied, a
noCopy
struct is embedded that will causego vet
to fail if copies are performed. Non-pointer fields are also moved to the end of the struct to improve allocation speed.Results
There are improvements in QPS, latency and allocs/op for unary RPCs.
Resources
RELEASE NOTES: