Conversation
When GTO removes fields or makes them immutable, it may introduce an immutable externref first field on the descriptor of a JS-exposed type where there was none before. That means that the described type could now have a JS-observable prototype where it did not before optimization, which makes this a misoptimization. Fix the problem by inserting an i8 placeholder first field wherever we would otherwise start exposing a prototype where there was none before. This is expected to be exceptionally rare in practice, so the extra memory use is not expected to be a real problem. Instead of adding a placeholder field, we could have inhibited optimization of the existing first field, but that would be more likely than an unaccessed placeholder field to have adverse effects in later passes. Fixes #9026.
| ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) | ||
| (type $struct (descriptor $desc) (struct)) | ||
| ;; CHECK: (type $desc (describes $struct) (struct (field i8) (field externref))) | ||
| (type $desc (describes $struct) (struct (field (mut externref)))) |
There was a problem hiding this comment.
It's not clear to me why you say this would be "exceptionally rare" in practice? Because externrefs are rare? Otherwise, a mutable one seems common, and in the first slot also seems common.
Externrefs to represent strings and other JS content may be pretty common in some situations?
There was a problem hiding this comment.
Because descriptors in practice are vtables, which I don't think are likely to contain strings or other externrefs. Maybe some toolchain would put a class name string in the vtable?
There was a problem hiding this comment.
I see, thanks, that's what I was missing. So this is just on descriptors, not general structs.
kripken
left a comment
There was a problem hiding this comment.
Also, I am uneasy with this tradeoff. The benefits of immutability are large in general, but less so for an externref field (there is no constant we can propagate). And adding an i8 is a definite cost. I would actually lean towards the opposite tradeoff?
|
That's for the case of a mutable externref field being made immutable, but what about the case where an externref field is shifted into the first position? |
|
We could also avoid shifting it there? Basically, changes that turn it into a prototype would be disallowed. You're right that I didn't consider shifting, so maybe this is more common/complex than I thought, but adding a field seems like a "heavy" operation... |
|
We could avoid shifting, but the effect would be at least as bad as the placeholder i8 because that would mean keeping whatever prefix of fields we would have removed that would have caused the externref to shift into the first position. I'm pretty happy with the placeholder field trade off because it doesn't inhibit any other optimizations we would do and works no matter what the invalid transformation would have been. Since we only add placeholders to descriptors, which are much rarer than normal objects at runtime, the memory overhead of the placeholder fields is expected to be tiny, even if we do end up needing lots of placeholders for some reason. |
kripken
left a comment
There was a problem hiding this comment.
Ok, fair points on the tradeoff, sg
| bool hasPlaceholder = false; | ||
|
|
||
| IndexAnalysis(const std::vector<Index>& indexes) { | ||
| Index maxIndex = 0; |
There was a problem hiding this comment.
Perhaps add some comments to this class and/or properties? Just reading the code, I'd expect newSize to mean the size after removals - is that right? And the input is the mapping of old indexes to new indexes?
There was a problem hiding this comment.
Yes, newSize is the size after removals and possible placeholder insertion. And yes, the input is the old to new mapping. I'll add comments.
| if (!removableIndexes.empty() || superHasUpdates || isExposedNoProto) { | ||
| // We might be removing fields. Reorder them to allow that, as in the | ||
| // general case we can only remove fields from the end, so that if our | ||
| // subtypes still need the fields they can append them. For example: |
There was a problem hiding this comment.
Please comment about the importance of isExposedNoProto to the placeholder issue
| if (auto it = canBecomeImmutable.find(type); | ||
| it != canBecomeImmutable.end() && i < it->second.size() && | ||
| it->second[i]) { | ||
| optimizedField.mutable_ = Immutable; |
There was a problem hiding this comment.
Why do we apply immutability here? That is, why does this duplicate the normal code that turns fields immutable?
There was a problem hiding this comment.
We need to check if the normal optimization would produce an immutable externref in the first field without our placeholder intervention. If we didn't check what normal optimization would do with immutability here, then we would either end up adding unnecessary placeholders when the first field would become a mutable externref or end up missing necessary placeholders when the first field is optimized from mutable to immutable.
There was a problem hiding this comment.
Can't we do this in that normal code, then?
I mean that it seems odd to have two places in the code that turn things immutable.
| for (auto& idx : indexesAfterRemoval) { | ||
| if (idx != RemovedField) { | ||
| ++idx; | ||
| } |
There was a problem hiding this comment.
Please add a comment that we do not add the i8 here, we just make room for it, and that room - the missing index 0 - is the marker we use to identify the need to add the i8 later (is that right?)
|
|
||
| // Also propagate exposed descriptors to supertypes so that descriptor | ||
| // hierarchies have consistent layouts. Do not propagate to supertypes that | ||
| // actually expose a prototype. |
There was a problem hiding this comment.
- Why didn't we need this code before?
- Why do we not propagate as per the second line here?
There was a problem hiding this comment.
We didn't need this code before because exposedNoProtoDescs is a new set that is used just to help determine when to add placeholders. I guess the comment should say "propagate lack of exposed descriptors."
The case where we stop propagating is the edge case where a subtype has nullexternref and the supertype has externref as its immutable first field. We do not want to add a placeholder to the supertype in that case because it actually does expose a prototype. I'll add a comment about that to the code.
| } | ||
| } | ||
| newSize = hasKept ? maxIndex + 1 : 0; | ||
| hasPlaceholder = hasKept && !hasIndexZero; |
There was a problem hiding this comment.
This seems a bit "magical" - worth a comment
| // we have done pointless work). | ||
| operands.resize(analysis.newSize); | ||
| if (analysis.hasPlaceholder) { | ||
| operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0))); |
There was a problem hiding this comment.
| operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0))); | |
| // What we put in the i8 placeholder does not matter. | |
| operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0))); |
When GTO removes fields or makes them immutable, it may introduce an immutable externref first field on the descriptor of a JS-exposed type where there was none before. That means that the described type could now have a JS-observable prototype where it did not before optimization, which makes this a misoptimization.
Fix the problem by inserting an i8 placeholder first field wherever we would otherwise start exposing a prototype where there was none before. This is expected to be exceptionally rare in practice, so the extra memory use is not expected to be a real problem.
Instead of adding a placeholder field, we could have inhibited optimization of the existing first field, but that would be more likely than an unaccessed placeholder field to have adverse effects in later passes.
Fixes #9026.