Repository navigation
[Perf][SwiftSyntax] Stop interleaving unexpected node slots unless needed - #3458
Conversation
4b19f37 to
0643d84
Compare
4df4dbb to
105276a
Compare
|
@swift-ci Please test |
|
@swift-ci Please test macOS |
| // Every slot is written exactly once — the `unexpected` ones exist | ||
| // only when they are being written — so initializing them and then | ||
| // assigning over them would be a `memset` of the whole tail for | ||
| // nothing. |
There was a problem hiding this comment.
This feels like an unnecessary comment for something the code doesn't do?
| if hasUnexpected { | ||
| layout.initializeElement(at: 1, to: unexpectedBeforeProvider?.raw) | ||
| layout.initializeElement(at: 2, to: unexpectedAfterProvider?.raw) | ||
| } |
There was a problem hiding this comment.
Nit: Would be nice to fix the formatting here
| /// Children with no `unexpected` slots among them: every collection, and the | ||
| /// layout kinds that do not interleave. | ||
| case flat | ||
| /// A kind that interleaves `unexpected` slots, in a node where every one of | ||
| /// them is empty, so it keeps room only for its real children. | ||
| case interleaved | ||
| /// A kind that interleaves `unexpected` slots, in a node where at least one is | ||
| /// occupied, so it keeps its real children and then all of them. | ||
| case interleavedWithUnexpected |
There was a problem hiding this comment.
This doesn't match the PR description, is that intentional? Given the storage isn't interleaved in these cases I think the names in the PR make more sense?
There was a problem hiding this comment.
Ah, previously I was embedding the storage kind into the header (RawSyntaxData), When I moved it to Layout.storage I renamed them, but I agree interleaved is confusing, so I renamed back to layout
| /// | ||
| /// - Precondition: this is a layout node or a collection. | ||
| @inline(__always) | ||
| var slotBase: (base: UnsafePointer<RawSyntax?>, childCount: Int) { |
There was a problem hiding this comment.
Any reason not to return a buffer pointer?
There was a problem hiding this comment.
Because childCount is not the actual count of the slot buffer, it does not include unexpected nodes in case of "with unexpected". But it's a bit confusing with Layout.Ref.slotBase, I'm just removing this accessor and make all the clients access slotBase and childCount separately.
| switch layout.storage { | ||
| case .interleavedWithUnexpected: | ||
| unexpected = UnsafeBufferPointer(start: start + childCount, count: childCount &+ 1) | ||
| interleaves = true | ||
| case .interleaved: | ||
| unexpected = UnsafeBufferPointer(start: nil, count: 0) | ||
| interleaves = true | ||
| case .flat: | ||
| unexpected = UnsafeBufferPointer(start: nil, count: 0) | ||
| interleaves = false | ||
| } |
There was a problem hiding this comment.
Is this not layoutView.interleavedRegions?
There was a problem hiding this comment.
Right, this is now built from interleavedRegions.
| initializingWith initializer: (UnsafeMutableBufferPointer<RawSyntax?>) -> Void | ||
| ) -> RawSyntax { | ||
| return .makeLayout(kind: kind, uninitializedCount: 0, arena: arena) { _ in } | ||
| // A layout node has at most 23 slots, so the temporary is small. |
There was a problem hiding this comment.
Just to make it clear it's not a structural property, just the current largest node
| // A layout node has at most 23 slots, so the temporary is small. | |
| // A layout node currently has at most 23 slots, so the temporary is small. |
| let interleaves = kind.interleavesUnexpectedChildren | ||
| // Real children are the odd slots when a kind interleaves, and all of them | ||
| // when it does not. | ||
| let childCount = interleaves ? (count - 1) / 2 : count |
There was a problem hiding this comment.
Should we assert that count is odd?
| public var endIndex: Int { self.interleaves ? 2 &* self.real.count &+ 1 : self.real.count } | ||
|
|
||
| public subscript(position: Int) -> RawSyntax? { | ||
| precondition(position >= 0 && position < self.endIndex) |
There was a problem hiding this comment.
Using precondition here differs from realChild/unexpectedChild, should we be consistent?
There was a problem hiding this comment.
Made it assert for consistency.
I originally was using precondition for realChild/unexpectedChild, but it slowed down. So I changed it to assert.
| /// One shape per function: a reader of this inlines whichever of the two it needs, | ||
| /// and neither is large enough to cost the inlining of what it calls. Both in one | ||
| /// function is 766 instructions where the two are 271 and 300, and at that size the | ||
| /// compiler stops inlining `advancedBySibling` — which then runs once per slot of | ||
| /// every node. |
There was a problem hiding this comment.
Can you trim this comment? The instruction counts are likely to become outdated, and advancedBySibling is @inline(__always) so not sure that rationale holds?
| if let elements = layoutView.flatSlots { | ||
| return createFlatLayoutData(parent, elements) | ||
| } | ||
| return createInterleavedLayoutData(parent, layoutView.interleavedRegions!, count: layoutView.logicalChildCount) |
There was a problem hiding this comment.
Any reason not to just pass the layoutView? I don't love passing tuples as arguments
There was a problem hiding this comment.
Performance, .flatSlots, .interleaveRegions and .logicalChildCount all have switch raw.storage in it. They are merged into one switch_enum only when they are all inlined.
There was a problem hiding this comment.
But I ended up with inlining both builders, as it turned out to be faster. Also the readability is fine.
105276a to
5aad887
Compare
A non-collection layout node interleaves an `unexpected` slot before its first child, between every pair and after the last, so n children take 2n + 1 slots. Over a 749 file corpus not one of 1,132,225 layout nodes had anything in any of them, and those slots were 56.8% of every layout child slot in the tree. A node keeps room for them only when one of them is occupied. The byte its fields had to spare says which: `.flat` for a node whose slots are all children, which is every collection and the layout kinds that opt out of interleaving, `.layout` for a node that kept only its real children, and `.layoutWithUnexpected` for a node that has something unexpected in it. The fields count real children rather than slots. The caller says which of the three it is building. The generated initializers know it statically — whether their kind has `unexpected` slots at all — and work out whether any of them is occupied, so every slot is written exactly once. The form that takes a layout as the tree describes it decides the shape from what it wrote, which is what every mutating operation goes through, so a rewritten node comes back as compact as its contents allow. Tests check that a node reached by mutating another cannot be told apart from the same node built outright, including after growing and shrinking repeatedly. A node's children are still what the tree describes, `unexpected` slots included, answering nil for a slot a node kept no room for. The readers that matter avoid working each position out: - The raw accessors ask for a real child or an `unexpected` slot by where it sits, with constant indices in the generated code, and check the index in debug builds only. So do the deprecated `add…` methods. - A collection's elements are read as its slots, and `firstToken` and `lastToken` visit only the children a node holds. - Collecting a tree's text, writing its description and scanning for line ends walk the slots a node kept rather than every position its kind names. - `SyntaxDataArena` writes a node's data buffer from the two regions a node keeps, so what lands in it, and every index a client uses to reach a child, is unchanged. - Building a node's data buffer reads a `.flat` node's slots as non-optional children without asking its kind. Over the corpus, a tree takes 97.9 MB where it took 136.5 MB, which is 10.15 times the source rather than 14.17. Fingerprints of all 749 trees are unchanged, walking every node with `viewMode: .all`.
5aad887 to
4d26df9
Compare
|
@swift-ci Please test |
|
@swift-ci Please test Windows |
Layout nodes always had
2n + 1child slots:nslots for their real children andn + 1interleaved slots for unexpected nodes. In practice, unexpected nodes are rare, so most of those slots are nil. This change avoids allocating the unexpected-node slots unless at least one of them is populated.RawSyntaxData.Layoutnow takes one of threeRawSyntaxData.Layout.Storageforms:.flat- A node with non-nil children; every collection andUnexpectedNodesSyntax.layout- A layout node without unexpected nodes, each slot can be nil.layoutWithUnexpected- A layout node that has something unexpected in it.RawSyntaxData.Layoutbecomes{ childCount: 4 bytes; byteLength: 4; descendantCount: 4; kind: 2; flags: 1; storage: 1; }, making it 16/16 bytes.The tail allocated child buffer depends on the storage kind:
.flatstores exactly the number of children, all as non-nilRawSyntax..layoutstores a fixedn-slot layout of "real" children asRawSyntax?..layoutWithUnexpectedstores the samenchild slots followed by a separate region ofn + 1unexpected-node slots.Result
Against
main(624d85e), two pairs, instructions and wall clock:syntaxTextBytesSourceLocationConverterdescriptionrawTextreadsSyntaxVisitorwalkchildren(viewMode:)walkmain__text)@_spi(RawSyntax)changesRawSyntaxLayoutView.childrenis aRawLayoutChildrenrather than aRawSyntaxBuffer. It's aRandomAccessCollectionof the children as the tree describes them.RawSyntaxLayoutView'srealChild(at:),unexpectedSlot(at:),flatSlots,elementsandinterleavedRegions;RawSyntaxElements; andSyntaxKind.interleavesUnexpectedChildren.