Skip to content

[Perf][SwiftSyntax] Stop interleaving unexpected node slots unless needed - #3458

Merged
rintaro merged 1 commit into
swiftlang:mainfrom
rintaro:perf-parser-35-compact-layout
Oct 11, 2026
Merged

rintaro merged 1 commit into
swiftlang:mainfrom
rintaro:perf-parser-35-compact-layout

Conversation

@rintaro

@rintaro rintaro commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Layout nodes always had 2n + 1 child slots: n slots for their real children and n + 1 interleaved 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.Layout now takes one of three RawSyntaxData.Layout.Storage forms:

  • .flat - A node with non-nil children; every collection and UnexpectedNodesSyntax
  • .layout - A layout node without unexpected nodes, each slot can be nil
  • .layoutWithUnexpected - A layout node that has something unexpected in it.

RawSyntaxData.Layout becomes { 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:

  • .flat stores exactly the number of children, all as non-nil RawSyntax.
  • .layout stores a fixed n-slot layout of "real" children as RawSyntax?.
  • .layoutWithUnexpected stores the same n child slots followed by a separate region of n + 1 unexpected-node slots.

Result

Against main (624d85e), two pairs, instructions and wall clock:

instructions, pair 1 / pair 2 wall clock, pair 1 / pair 2
parse: MinimalCollections −0.80% / −0.78% −2.3% / −2.4%
decl_heavy −0.82% / −0.86% −6.2% / −5.0%
nonascii_heavy −0.68% / −0.71% −2.7% / −2.6%
corrupt_heavy +0.01% / −0.01% +0.2% / −0.1%
text consumers: syntaxTextBytes −9.77% / −9.77% −9.1% / −7.3%
SourceLocationConverter −7.61% / −7.61% −9.6% / −11.2%
description −1.32% / −1.33% +0.8% / −2.2%
reads: typed accessors +0.19% / +0.58% −0.9% / +1.5%
rawText reads +0.24% / +0.73% −1.7% / +0.9%
empty SyntaxVisitor walk +1.41% / +1.43% −2.3% / −1.6%
children(viewMode:) walk +0.74% / +0.78% +1.2% / +5.4%
tree memory over the 749 files main PR change
a tree's size against its source 14.17× 10.15× −28.3%
bytes allocated 136.5 MB 97.9 MB −28.3%
slab memory taken 146.9 MB 107.2 MB −27.0%
code size (__text) change
SwiftSyntax −2,968 bytes (−0.1%)
SwiftParser +164 bytes (+0.0%)

@_spi(RawSyntax) changes

  • RawSyntaxLayoutView.children is a RawLayoutChildren rather than a RawSyntaxBuffer. It's a RandomAccessCollection of the children as the tree describes them.
  • Added: RawSyntaxLayoutView's realChild(at:), unexpectedSlot(at:), flatSlots, elements and interleavedRegions; RawSyntaxElements; and SyntaxKind.interleavesUnexpectedChildren.

Comment thread Sources/SwiftSyntax/Raw/RawSyntax.swift
@rintaro
rintaro force-pushed the perf-parser-35-compact-layout branch 2 times, most recently from 4b19f37 to 0643d84 Compare October 4, 2026 14:26
@rintaro rintaro changed the title [Perf][SwiftSyntax] Tail allocate RawSyntax layout buffer, without interleaving unexpected node slots unless needed [Perf][SwiftSyntax] Stop interleaving unexpected node slots unless needed Oct 5, 2026
@rintaro
rintaro force-pushed the perf-parser-35-compact-layout branch 4 times, most recently from 4df4dbb to 105276a Compare October 6, 2026 00:46
@rintaro
rintaro marked this pull request as ready for review October 6, 2026 00:51
@rintaro

rintaro commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@swift-ci Please test

@rintaro

rintaro commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@swift-ci Please test macOS

Comment on lines +168 to +171
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like an unnecessary comment for something the code doesn't do?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines +123 to +126
if hasUnexpected {
layout.initializeElement(at: 1, to: unexpectedBeforeProvider?.raw)
layout.initializeElement(at: 2, to: unexpectedAfterProvider?.raw)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Would be nice to fix the formatting here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment thread Sources/SwiftSyntax/Raw/RawSyntax.swift Outdated
Comment on lines +127 to +135
/// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread Sources/SwiftSyntax/Raw/RawSyntax.swift Outdated
///
/// - Precondition: this is a layout node or a collection.
@inline(__always)
var slotBase: (base: UnsafePointer<RawSyntax?>, childCount: Int) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to return a buffer pointer?

@rintaro rintaro Oct 10, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Sources/SwiftSyntax/Raw/RawSyntax.swift Outdated
Comment on lines +606 to +616
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not layoutView.interleavedRegions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is now built from interleavedRegions.

Comment thread Sources/SwiftSyntax/Raw/RawSyntax.swift Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make it clear it's not a structural property, just the current largest node

Suggested change
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using precondition here differs from realChild/unexpectedChild, should we be consistent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it assert for consistency.
I originally was using precondition for realChild/unexpectedChild, but it slowed down. So I changed it to assert.

Comment thread Sources/SwiftSyntax/Syntax.swift Outdated
Comment on lines +460 to +464
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you trim this comment? The instruction counts are likely to become outdated, and advancedBySibling is @inline(__always) so not sure that rationale holds?

Comment thread Sources/SwiftSyntax/Syntax.swift Outdated
if let elements = layoutView.flatSlots {
return createFlatLayoutData(parent, elements)
}
return createInterleavedLayoutData(parent, layoutView.interleavedRegions!, count: layoutView.logicalChildCount)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to just pass the layoutView? I don't love passing tuples as arguments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I ended up with inlining both builders, as it turned out to be faster. Also the readability is fine.

@rintaro
rintaro force-pushed the perf-parser-35-compact-layout branch from 105276a to 5aad887 Compare October 11, 2026 03:07
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`.
@rintaro
rintaro force-pushed the perf-parser-35-compact-layout branch from 5aad887 to 4d26df9 Compare October 11, 2026 03:31
@rintaro

rintaro commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

@swift-ci Please test

@rintaro

rintaro commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

@swift-ci Please test Windows

@rintaro
rintaro merged commit 8453c53 into swiftlang:main Oct 11, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants