-
Notifications
You must be signed in to change notification settings - Fork 47
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
Improve the performance of Go generated marshaling/encoding code #65
Conversation
#336) Update the version of stellar/go used in this repo to include the new commits that contain the xdr generated with stellar/xdrgen#65. stellar/xdrgen#65 improves the performance of xdr encoding.
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.
LGTM! It should improve ingestion speed in Horizon. Probably fixes stellar/go#2689 and stellar/go#3256.
out.puts " }" | ||
end | ||
when :array | ||
out.puts " for i := 0; i < len(#{var}); i++ {" |
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 check max_length
tag here? I'm not sure if the previous code checks this but if it does we probably should have it here too.
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 it is necessary because the type of var
in the :array
case is a fixed size array, and in Go we will represent it as an array, which has a fixed size at compile time. For example:
However, we could check max_length for the :var_array
case which we represent as a Go slice and therefore has an unbounded size. The code that does encoding of variable length arrays today (src: https://github.com/stellar/go-xdr/blob/b95df30963cd0a4fcd12f2d65db0f9aeba987f73/xdr3/encode.go#L404-L427) doesn't check the max length, so that would be a functional change. I think if we are to pursue that we should pursue that separately to this PR.
Note that this PR doesn't improve unmarshaling, so there may be more we can squeeze out in the future. |
Right, but |
I'm wondering if this is an opportunity to test this new marshaling code. How would we test that the marshaling is consistent in Horizon with this change? If Horizon marshals XDR during ingestion, if we rebuilt Horizon using xdr generated by this PR, and ran a full history ingestion, would that be sufficient at identifying any problems marshaling that data? Would Horizon error if the marshaling resulted in different XDR? |
We can easily confirm this by |
Also, it looks like staticcheck found some issues in generated code: https://github.com/stellar/go/pull/3957/checks?check_run_id=3705045608 |
Yup, and this is really annoying, because it is much easier to generate the code if we write it this way. I think we should probably stop running linters on generated code, or at least disable some linters like this that really have zero utility for generated code. |
@bartekn I got stellar/go#3957 lint errors fixed. Could you help with running a verify-range test using that PR? |
@leighmcculloch the verify-range did not find any issues. Do you think we can merge this and stellar/go#3957? I'm looking forward for this performance bump. |
@leighmcculloch just to clarify, we will happily make a Horizon release just for this. The sooner we merge it the sooner we will release it :) |
@bartekn @2opremio I'm good to merge this, however @bartekn left a comment stellar/go#3957 (comment) recommending we also use randxdr to fuzz the generated code. I think that could be a good idea, but I don't have capacity to handle that immediately. Is that something you could help out with? |
@leighmcculloch where is the |
@2opremio It is embedded in the PR description. Click the little arrow next to |
I know this arrives very late ... but wouldn't it be better to use: type xdrEncodable interface {
EncodeTo(e *xdr.Encoder) error
}
// Marshal writes an xdr element `v` into `w`.
func Marshal(w io.Writer, v interface{}) (int, error) {
if encodable, ok := v.(xdrEncodable); ok {
b := bytes.Buffer{}
e := xdr.NewEncoder(&b)
err := encodable.EncodeTo(e)
return w.Write( b.Bytes())
}
// delegate to xdr package's Marshal
return xdr.Marshal(w, v)
} instead of: type xdrType interface {
xdrType()
}
// Marshal writes an xdr element `v` into `w`.
func Marshal(w io.Writer, v interface{}) (int, error) {
if _, ok := v.(xdrType); ok {
if bm, ok := v.(encoding.BinaryMarshaler); ok {
b, err := bm.MarshalBinary()
if err != nil {
return 0, err
}
return w.Write(b)
}
}
// delegate to xdr package's Marshal
return xdr.Marshal(w, v)
} Using |
That looks cleaner. My only hesistancy is I wouldn't want to move the bytes.Buffer up to a generic function disconnected from the type, because I hope to replace bytes.Buffer with fixed sized buffers based on max size of the types, and we can only do that if the buffer is defined inside the MarshalBinary, and if xdr.Marshal calls MarshalBinary. We could still test on xdr.xdrEncoderable, but then call .MarshalBinary, but that doesn't completely get rid of the oddity. |
I don't think we need fixed buffers, see stellar/go#4056 |
Update XDR using stellar/xdrgen#65 that improves the encoding cpu and memory usage.
What
Change how the generated Go code marshals objects to binary to move logic from runtime to compile time. Specifically:
EncodeTo
function to all types that takes anxdr.Encoder
and uses the encoder to encode itself.EncodeTo
functions call the appropriate lower level encode function for their type, such asEncodeUint
, instead ofEncode
that will use reflection to figure out the type.EncodeTo
functions of complex types (structs, unions) call theEncodeTo
function of each of their fields.For any types not generated, such as
time.Time
, encoding falls back to the current reflection based approach.This change affects marshaling/encoding only, and not unmarshaling/decoding.
Example
For an example of how this changes generated Go code, see stellar/go#3957.
Why
I noticed while working on stellar-deprecated/starlight#40 that the application was spending as much time building transactions as it was ed25519 verifying which is a bit of a red flag given that XDR's simple and non-self-descriptive format should be performant.
On inspection the code generated relies on reflection for encoding all types. Reflection is known to not be very performant and results in allocations that would be otherwise unnecessary.
A simple benchmark of calling
MarshalBinary
onxdr.TransactionEnvelope
fromgithub.aaakk.us.kg/stellar/go/xdr
demonstrates this well. The same benchmark running against code generated by this pull request significantly reduces encoding time and allocations.Before:
After:
Benchmark code and some additional tests run inside stellar/go
This change only focuses on marshaling/encoding because unmarshaling is significantly more complex and not required for the use cases I'm interested, building and hashing transactions.
Optional types such as
SponsorshipDescriptor
from the Stellar XDR complicate this encoding somewhat, although not significantly.Known Limitations
N/A