-
Notifications
You must be signed in to change notification settings - Fork 499
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
Create xdr.EncodingBuffer, which reduces buffer allocations #4056
Create xdr.EncodingBuffer, which reduces buffer allocations #4056
Conversation
7e0e5e7
to
ce099d4
Compare
@bartekn I didn't replace every Marshaling invocation with the new |
ce099d4
to
87d2cbe
Compare
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.
😎 Very cool, halving encoding time is great. One comment about where this new type lives.
I defer to @stellar/horizon-committers for the Horizon changes.
Note how xdr encoding now smashes the performance of the gxdr encoder (which used to be better than the xdr encoder).
I think the xdr package is already much faster than gxdr without this change, because of the stellar/xdrgen#65 changes, but this change makes it better, half is awesome.
xdr/main.go
Outdated
// Encoder reuses internal buffers between invocations | ||
// to minimize allocations. | ||
type Encoder struct { | ||
encoder *xdr.Encoder | ||
xdrEncoderBuf bytes.Buffer | ||
otherEncodersBuf []byte | ||
} |
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 type looks like something that should live in the go-xdr package, we could replace the existing Encoder there with this, or add it there as a new type ScratchEncoder
, or something similar. Having it here be xdr.Encoder
is a little confusing, there's no indication of when to use this over the one in the go-xdr package. So comments would benefit, and I think moving it to go-xdr would be best.
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.
The encoder also takes care of bookeeping buffers for base64 and hex encoding which I don't think belongs to go-xdr
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 think I agree with @leighmcculloch. If we move the code to go-xdr
we can still do base64/hex stuff here.
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 think we can move the base64 to go-xdr too. Base64ing xdr is generally helpful.
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.
2x speed improvement? WOW! 😮 LGreatTM!
@bartekn I didn't replace every Marshaling invocation with the new xdr.Encoder, let me know if there are place you think I missed
AFAIR, ChangeCompactor
takes a bunch of time during ingestion.
otherEncodersBuf []byte | ||
} | ||
|
||
func growSlice(old []byte, newSize int) []byte { |
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'm not sure why we use bytes.Buffer
for xdr encoder but this custom code for other encoders?
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.
@2opremio I think we can get rid of the extra base64 buffer and this grow code by streaming through the base64 encoder.
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.
The base64 (and hex) encoders need a buffer to write to. This would allow us to get rid of the grow code, but not the buffer
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.
Also, base64 (and hex) the encoders don't have a reset function and we don't want to allocate a new encoder every time.
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'm pretty sure you can reuse them after calling Close. For example:
b1 := strings.Builder{}
e1 := base64.NewEncoder(base64.StdEncoding, &b1)
e1.Write([]byte("Hello World"))
e1.Close()
fmt.Println(b1.String())
b1.Reset()
e1.Write([]byte("Hello World"))
e1.Close()
fmt.Println(b1.String())
Ref: https://play.golang.org/p/kDifIOFjblC
Note the use of a strings.Builder, which might be better than writing to a []byte and converting it into a string. So maybe you choose in the moment which type of buffer you use dependent on whether you want binary or base64. Use a bytes.Buffer with no base64 encoder when you want binary. Use strings.Builder with a base64 encoder when you want a string.
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.
The strings builder won't perform well because (from what I have read) resetting dereferences the intermediate buffer, causing a allocations in every encoding invocation.
xdr/main.go
Outdated
// Encoder reuses internal buffers between invocations | ||
// to minimize allocations. | ||
type Encoder struct { | ||
encoder *xdr.Encoder | ||
xdrEncoderBuf bytes.Buffer | ||
otherEncodersBuf []byte | ||
} |
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 think I agree with @leighmcculloch. If we move the code to go-xdr
we can still do base64/hex stuff here.
xdr/main.go
Outdated
// Subsequent calls to marshalling methods will overwrite the returned buffer. | ||
func (e *Encoder) UnsafeMarshalBinary(v interface{}) ([]byte, error) { | ||
e.xdrEncoderBuf.Reset() | ||
if encodable, ok := v.(xdrEncodable); ok { |
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 think if we move the logic in this function to go-xdr, this xdr.Encider type can disappear if instead of base64ing a byte slice, you store a bytes.Buffer in all the places you're storing the xdr.Encoder, and wrap it in a base64.Encoder whenever writing to it. This way you don't need two buffers, only one. Also we don't need any new xdr encoder types. We make the existing xdr encoder type better by using the generated EncodeTo functions when they're present.
I have been thinking about it and I disagree about incorporating hex and base64 decoding into go-xdr. It breaks the separation of concerns. For instance, using base64 decoding there wouldn't allow us to implement #4052 easily. I am happy to change the name of the encoder to @leighmcculloch I am fine with moving the |
I agree, we don't need to add hex or base64 encoding to go-xdr. If you move the EncodeTo use to go-xdr, this PR can change to simply embed a bytes.Buffer into the Horizon types, and use https://pkg.go.dev/encoding/base64#NewEncoder, and nothing needs to change in the |
What types? |
I don't see how we can do this without changing the xdr package, please elaborate |
d2ce1cd
to
0d8c529
Compare
I'm doing a poor job getting my suggestion across, so I'll try to detail it. I think if we add import xdr3 "github.com/stellar/go-xdr/xdr3"
buf := strings.Builder{}
b64 := base64.NewEncoder(base64.StdEncoding, &buf)
enc := xdr3.NewEncoder(&b64)
enc.Encode(xdrObj)
b64.Close()
str := buf.String() // <<< Base64 XDR encoded as a string
buf.Reset() You could store That does seem cumbersome though, so yeah I think you are correct, a type in the xdr package still makes sense. That type could be slimmer than what's here though, and use a single buffer like above. type StringEncoder struct {
w strings.Builder
enc xdr3.Encoder
flush func()
}
func NewBase64StringEncoder() *StringEncoder {
enc := &ResetEncoder{}
b64 := base64.NewEncoder(base64.StdEncoding, enc.w)
enc.enc = xdr3.NewEncoder(&b64)
enc.flush = b64.Close
return enc
}
func NewHexStringEncoder() *StringEncoder {
enc := &ResetEncoder{}
h := hex.NewEncoder(enc.w)
enc.enc = xdr3.NewEncoder(h)
enc.flush = func() {}
return enc
}
func (e *StringEncoder) EncodeToString(v interface{}) (string, error) {
err := e.Encode(v)
if err != nil {
return "", err
}
str := e.String()
e.Reset()
return str, nil
}
func (e *StringEncoder) Encode(v interface{}) error {
return e.enc.Encode(v)
}
func (e *StringEncoder) String() string {
e.flush()
return e.w.String()
}
func (e *StringEncoder) Reset() {
e.flush()
e.w.Reset()
} Which can then be used like: type ClaimableBalancesChangeProcessor struct {
stringEncoder *xdr.StringEncoder
// ...
}
p := &ClaimableBalancesChangeProcessor{
stringEncoder: xdr.NewHexStringEncoder(),
// ...
}
id, err := p.stringEncoder.EncodeToString(cBalance.BalanceId) You could have a similar I think something like this would work to reduce the buffers, etc. Let me know if I'm way off base. Feel free to go with what you've got though if this doesn't address the problem. |
OK, much more clear. I will take it into consideration! |
864e6a4
to
f3e20d3
Compare
@leighmcculloch thanks for the suggestions, but I don't think that a strings builder is usable since resetting it deallocates it's internal buffer, causing allocations in every encoding call. |
See golang/go#24716 |
The same pattern should work with a bytes.Buffer? |
Yeah, but then I don't really see an advantage compared to the existing code. In fact, I suspect it may be less performant. I will give it a try though. |
I moved the logic of |
I will update xdrgen to generate comply with the new |
Actually, I don't think stellar/go-xdr#17 is worth the pain since it disallows unmarshaling by pointer in unaddressable values, impacting performance. I will merge as is. |
See stellar/xdrgen#67 (comment) for details. |
It reduces a few a allocations but it seems to reduce CPU consumption by ~7% ``` goos: darwin goarch: amd64 pkg: github.com/stellar/go/benchmarks cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz BenchmarkXDRMarshal BenchmarkXDRMarshal-8 102685 11346 ns/op 5128 B/op 156 allocs/op BenchmarkXDRMarshalWithEncoder BenchmarkXDRMarshalWithEncoder-8 106569 10636 ns/op 3920 B/op 150 allocs/op BenchmarkXDRMarshalHex BenchmarkXDRMarshalHex-8 94836 12340 ns/op 6920 B/op 158 allocs/op BenchmarkXDRMarshalHexWithEncoder BenchmarkXDRMarshalHexWithEncoder-8 104115 11393 ns/op 4816 B/op 151 allocs/op BenchmarkXDRUnsafeMarshalHexWithEncoder BenchmarkXDRUnsafeMarshalHexWithEncoder-8 104821 11024 ns/op 3920 B/op 150 allocs/op BenchmarkXDRMarshalBase64 BenchmarkXDRMarshalBase64-8 96364 12162 ns/op 6280 B/op 158 allocs/op BenchmarkXDRMarshalBase64WithEncoder BenchmarkXDRMarshalBase64WithEncoder-8 104962 11236 ns/op 4496 B/op 151 allocs/op BenchmarkXDRUnsafeMarshalBase64WithEncoder BenchmarkXDRUnsafeMarshalBase64WithEncoder-8 102901 11042 ns/op 3920 B/op 150 allocs/op ```
f3e20d3
to
b434d7d
Compare
d0e0784
to
8494f90
Compare
The improvement is considerable. It reduces the allocations (and CPU consumption) by roughly half.
Note how xdr encoding now smashes the performance of the gxdr encoder (which used to be better than the xdr encoder).