-
Notifications
You must be signed in to change notification settings - Fork 460
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
Revisit when to use heap based storage for messages. #735
Comments
I believe the only case where we have to use storage is if there is a non repeated field that is transitively recursive. Any other limits are about how large we want to let the struct before we push it into the heap to try and cut down on the cost of the struct copying. A repeated or map field already will be using some heap space, so it probably comes down to how many of those and how many primitive fields we want. While floating ideas - we could change things, and only put the single fields in the storage. The repeated fields and map fields already be doing similar things under the hood to avoid copies. |
A relevant size constraint here is that putting a value type into an existential container will allocate if the value type is wider than 3 words (24 bytes). This means any attempt to put a message into a The SwiftNIO team tend to use 24 bytes as the maximum size of an inline struct when there is any risk of that structure being placed into an existential container. A related concern is reference counting: structs with multiple refcounted fields must refcount them all separately whenever their copy operation is invoked. For wide structs with many |
Interesting, don't It likely means we might also need to consider |
@Lukasa are the additional refcounts more expensive than the alloc/free operations for boxed storage? |
Nope. All of those types are smaller:
Both
This philosophy has carried into SwiftNIO: anything that gets passed around our
@ydnar The answer is "it depends". To a first order approximation, no, but in reality it depends on a number of factors, including how often the object is copied and how many copies the Swift compiler can manage to prove don't need to incur the refcounting cost. The answer can only really be determined on a per-workload basis by measuring the cost. This is a really unfortunate trade-off, and so normally I'd focus on the existential boxing concern. However, bear in mind that there are pathological cases. If you have a protocol buffer with 100 |
@Lukasa thanks for the valuable context. I’ll be sure to mind the size of our structs to <= 24 bytes going forward. PR (#900) was in part inspired by earlier work I did in a Go project to reduce GC pressure. There were analogous tradeoffs (copying structs vs pointers, mark + sweep penalty for string copies). I’d love to see the OP of #733 test this with his other pathological case (hundreds of thousands of messages). @thomasvl maybe it makes sense to consider the number of |
Yea, what's I've done for other projects like this is make the fields compute their "cost", and then rather then using the current field cost, we actually look at the instance size, and use that for when to flip over to storage. The calc is a little more tricky with If we're willing to go over the 24 bytes, then it might also make sense to factor in the fields that will include a retain to put some limit on those. The 24 is likely to be tight with unknown fields and extensions, but for all the reasons mentioned, moving those to their own storage and lazy creating it might be a good tradeoff (one pointer to hose both, with the home they they generally aren't needed, but take the hit once when either is used). |
So move unknown fields and extensions to lazily-created storage? Would a message be required to support unknown fields and/or extensions? I presume this would take at least a word’s worth of space? |
Only proto2 syntax messages support extensions, but all messages support unknown files. Right now, I believe we are inlining the fields. By moving those into their own storage we could collapse them down to a single pointer if either is needed. And we might be able to change things so they are created on demand as part of mutations rather than always having to create them; thus avoiding the allocations/refcounts/etc. until an individual message actually needs either one. |
- Push generation from the MessageGenerator into the sub Generators so the message generator has less knowledge about field details. - Move some of the proto extensions onto the Descriptor objects. - Expose direct knowledge to the sub Generators if Storage will be used. - Use Descriptors in more cases. - Expose oneofIndex on OneofGenerator
Related: #1034 In 1.10.2 many of the messages in our project end up with sizes over 200 bytes and some grow to over 5000 bytes. I have to revert to 1.7 where they were almost all 24 bytes. |
Look at the recursive cost of message fields also, so a nesting of messages all just under the limit doesn't result in a top level message over the limit. This also addresses some of the issues from apple#1034 and hopefully from apple#1014. This is also work on apple#735
#1046 won't count as a "fix" for this because it only incrementally handles singular sub message in some of the decisions. It does start to lay the ground work for factoring in a cost to message and messages used as fields, that this issue could better improve to try and determine real costs of things, and also factor in thing like extension field storage and unknown field storage. |
Spun out of discussions on #733
The current logic for deciding if a message should use heap storage is:
I believe the
hasSingleMessageField
is really there to handle transitive recursion back to this same message (protos making a tree structure).The text was updated successfully, but these errors were encountered: