Skip to content

Commit 62a352d

Browse files
authored
Merge pull request #2726 from rintaro/perf-rewriter
[Perf] Improve SyntaxRewriter visitation performance
2 parents 095c7d3 + ec96b4d commit 62a352d

File tree

8 files changed

+1084
-1156
lines changed

8 files changed

+1084
-1156
lines changed

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxRewriterFile.swift

Lines changed: 61 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4242
"""
4343
)
4444

45+
DeclSyntax(
46+
"""
47+
/// 'Syntax' object factory recycling 'Syntax.Info' instances.
48+
private let nodeFactory: SyntaxNodeFactory = SyntaxNodeFactory()
49+
"""
50+
)
51+
4552
DeclSyntax(
4653
"""
4754
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
@@ -65,7 +72,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6572
"""
6673
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
6774
public func rewrite(_ node: some SyntaxProtocol, detach: Bool = false) -> Syntax {
68-
let rewritten = self.dispatchVisit(Syntax(node))
75+
var rewritten = Syntax(node)
76+
self.dispatchVisit(&rewritten)
6977
if detach {
7078
return rewritten
7179
}
@@ -126,15 +134,19 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
126134
/// - Returns: the rewritten node
127135
@available(*, deprecated, renamed: "rewrite(_:detach:)")
128136
public func visit(_ node: Syntax) -> Syntax {
129-
return dispatchVisit(node)
137+
var rewritten = node
138+
dispatchVisit(&rewritten)
139+
return rewritten
130140
}
131141
"""
132142
)
133143

134144
DeclSyntax(
135145
"""
136146
public func visit<T: SyntaxChildChoices>(_ node: T) -> T {
137-
return dispatchVisit(Syntax(node)).cast(T.self)
147+
var rewritten = Syntax(node)
148+
dispatchVisit(&rewritten)
149+
return rewritten.cast(T.self)
138150
}
139151
"""
140152
)
@@ -148,7 +160,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
148160
/// - Returns: the rewritten node
149161
\(node.apiAttributes())\
150162
open func visit(_ node: \(node.kind.syntaxType)) -> \(node.kind.syntaxType) {
151-
return visitChildren(node)
163+
return visitChildren(node._syntaxNode).cast(\(node.kind.syntaxType).self)
152164
}
153165
"""
154166
)
@@ -160,7 +172,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
160172
/// - Returns: the rewritten node
161173
\(node.apiAttributes())\
162174
open func visit(_ node: \(node.kind.syntaxType)) -> \(node.baseType.syntaxBaseName) {
163-
return \(node.baseType.syntaxBaseName)(visitChildren(node))
175+
return \(node.baseType.syntaxBaseName)(visitChildren(node._syntaxNode).cast(\(node.kind.syntaxType).self))
164176
}
165177
"""
166178
)
@@ -177,7 +189,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
177189
/// - Returns: the rewritten node
178190
\(baseNode.apiAttributes())\
179191
public func visit(_ node: \(baseKind.syntaxType)) -> \(baseKind.syntaxType) {
180-
return dispatchVisit(Syntax(node)).cast(\(baseKind.syntaxType).self)
192+
var node: Syntax = Syntax(node)
193+
dispatchVisit(&node)
194+
return node.cast(\(baseKind.syntaxType).self)
181195
}
182196
"""
183197
)
@@ -187,21 +201,16 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
187201
"""
188202
/// Interpret `node` as a node of type `nodeType`, visit it, calling
189203
/// the `visit` to transform the node.
204+
@inline(__always)
190205
private func visitImpl<NodeType: SyntaxProtocol>(
191-
_ node: Syntax,
206+
_ node: inout Syntax,
192207
_ nodeType: NodeType.Type,
193208
_ visit: (NodeType) -> some SyntaxProtocol
194-
) -> Syntax {
195-
let castedNode = node.cast(NodeType.self)
196-
// Accessing _syntaxNode directly is faster than calling Syntax(node)
197-
visitPre(node)
198-
defer {
199-
visitPost(node)
200-
}
201-
if let newNode = visitAny(node) {
202-
return newNode
203-
}
204-
return Syntax(visit(castedNode))
209+
) {
210+
let origNode = node
211+
visitPre(origNode)
212+
node = visitAny(origNode) ?? Syntax(visit(origNode.cast(NodeType.self)))
213+
visitPost(origNode)
205214
}
206215
"""
207216
)
@@ -242,26 +251,26 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
242251
/// that determines the correct visitation function will be popped of the
243252
/// stack before the function is being called, making the switch's stack
244253
/// space transient instead of having it linger in the call stack.
245-
private func visitationFunc(for node: Syntax) -> ((Syntax) -> Syntax)
254+
private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
246255
"""
247256
) {
248257
try SwitchExprSyntax("switch node.raw.kind") {
249258
SwitchCaseSyntax("case .token:") {
250-
StmtSyntax("return { self.visitImpl($0, TokenSyntax.self, self.visit) }")
259+
StmtSyntax("return { self.visitImpl(&$0, TokenSyntax.self, self.visit) }")
251260
}
252261

253262
for node in NON_BASE_SYNTAX_NODES {
254263
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
255-
StmtSyntax("return { self.visitImpl($0, \(node.kind.syntaxType).self, self.visit) }")
264+
StmtSyntax("return { self.visitImpl(&$0, \(node.kind.syntaxType).self, self.visit) }")
256265
}
257266
}
258267
}
259268
}
260269

261270
DeclSyntax(
262271
"""
263-
private func dispatchVisit(_ node: Syntax) -> Syntax {
264-
return visitationFunc(for: node)(node)
272+
private func dispatchVisit(_ node: inout Syntax) {
273+
visitationFunc(for: node)(&node)
265274
}
266275
"""
267276
)
@@ -272,15 +281,15 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
272281
poundKeyword: .poundElseToken(),
273282
elements: .statements(
274283
CodeBlockItemListSyntax {
275-
try! FunctionDeclSyntax("private func dispatchVisit(_ node: Syntax) -> Syntax") {
284+
try! FunctionDeclSyntax("private func dispatchVisit(_ node: inout Syntax)") {
276285
try SwitchExprSyntax("switch node.raw.kind") {
277286
SwitchCaseSyntax("case .token:") {
278-
StmtSyntax("return visitImpl(node, TokenSyntax.self, visit)")
287+
StmtSyntax("return visitImpl(&node, TokenSyntax.self, visit)")
279288
}
280289

281290
for node in NON_BASE_SYNTAX_NODES {
282291
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
283-
StmtSyntax("return visitImpl(node, \(node.kind.syntaxType).self, visit)")
292+
StmtSyntax("return visitImpl(&node, \(node.kind.syntaxType).self, visit)")
284293
}
285294
}
286295
}
@@ -293,9 +302,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
293302

294303
DeclSyntax(
295304
"""
296-
private func visitChildren<SyntaxType: SyntaxProtocol>(
297-
_ node: SyntaxType
298-
) -> SyntaxType {
305+
private func visitChildren(_ node: Syntax) -> Syntax {
299306
// Walk over all children of this node and rewrite them. Don't store any
300307
// rewritten nodes until the first non-`nil` value is encountered. When this
301308
// happens, retrieve all previous syntax nodes from the parent node to
@@ -305,73 +312,48 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
305312
306313
// newLayout is nil until the first child node is rewritten and rewritten
307314
// nodes are being collected.
308-
var newLayout: ContiguousArray<RawSyntax?>?
309-
310-
// Rewritten children just to keep their 'SyntaxArena' alive until they are
311-
// wrapped with 'Syntax'
312-
var rewrittens: ContiguousArray<Syntax> = []
315+
var newLayout: UnsafeMutableBufferPointer<RawSyntax?> = .init(start: nil, count: 0)
313316
314-
let syntaxNode = node._syntaxNode
317+
// Keep 'SyntaxArena' of rewritten nodes alive until they are wrapped
318+
// with 'Syntax'
319+
var rewrittens: ContiguousArray<RetainedSyntaxArena> = []
315320
316-
// Incrementing i manually is faster than using .enumerated()
317-
var childIndex = 0
318-
for (raw, info) in RawSyntaxChildren(syntaxNode) {
319-
defer { childIndex += 1 }
320-
321-
guard let child = raw, viewMode.shouldTraverse(node: child) else {
322-
// Node does not exist or should not be visited. If we are collecting
323-
// rewritten nodes, we need to collect this one as well, otherwise we
324-
// can ignore it.
325-
if newLayout != nil {
326-
newLayout!.append(raw)
327-
}
328-
continue
329-
}
321+
for case let (child?, info) in RawSyntaxChildren(node) where viewMode.shouldTraverse(node: child) {
330322
331323
// Build the Syntax node to rewrite
332-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
324+
var childNode = nodeFactory.create(parent: node, raw: child, absoluteInfo: info)
333325
334-
let rewritten = dispatchVisit(Syntax(absoluteRaw, parent: syntaxNode))
335-
if rewritten.id != info.nodeId {
326+
dispatchVisit(&childNode)
327+
if childNode.raw.id != child.id {
336328
// The node was rewritten, let's handle it
337-
if newLayout == nil {
329+
330+
if newLayout.baseAddress == nil {
338331
// We have not yet collected any previous rewritten nodes. Initialize
339-
// the new layout with the previous nodes of the parent. This is
340-
// possible, since we know they were not rewritten.
341-
342-
// The below implementation is based on Collection.map but directly
343-
// reserves enough capacity for the entire layout.
344-
newLayout = ContiguousArray<RawSyntax?>()
345-
newLayout!.reserveCapacity(node.raw.layoutView!.children.count)
346-
for j in 0..<childIndex {
347-
newLayout!.append(node.raw.layoutView!.children[j])
348-
}
332+
// the new layout with the previous nodes of the parent.
333+
newLayout = .allocate(capacity: node.raw.layoutView!.children.count)
334+
_ = newLayout.initialize(fromContentsOf: node.raw.layoutView!.children)
349335
}
350336
351-
// Now that we know we have a new layout in which we collect rewritten
352-
// nodes, add it.
353-
rewrittens.append(rewritten)
354-
newLayout!.append(rewritten.raw)
355-
} else {
356-
// The node was not changed by the rewriter. Only store it if a previous
357-
// node has been rewritten and we are collecting a rewritten layout.
358-
if newLayout != nil {
359-
newLayout!.append(raw)
360-
}
337+
// Update the rewritten child.
338+
newLayout[Int(info.indexInParent)] = childNode.raw
339+
// Retain the syntax arena of the new node until it's wrapped with Syntax node.
340+
rewrittens.append(childNode.raw.arenaReference.retained)
361341
}
342+
343+
// Recycle 'childNode.info'
344+
nodeFactory.dispose(&childNode)
362345
}
363346
364-
if let newLayout {
347+
if newLayout.baseAddress != nil {
365348
// A child node was rewritten. Build the updated node.
366349
367-
// Sanity check, ensure the new children are the same length.
368-
precondition(newLayout.count == node.raw.layoutView!.children.count)
369-
370350
let arena = self.arena ?? SyntaxArena()
371-
let newRaw = node.raw.layoutView!.replacingLayout(with: Array(newLayout), arena: arena)
351+
let newRaw = node.raw.layoutView!.replacingLayout(with: newLayout, arena: arena)
352+
newLayout.deinitialize()
353+
newLayout.deallocate()
372354
// 'withExtendedLifetime' to keep 'SyntaxArena's of them alive until here.
373355
return withExtendedLifetime(rewrittens) {
374-
Syntax(raw: newRaw, rawNodeArena: arena).cast(SyntaxType.self)
356+
Syntax(raw: newRaw, rawNodeArena: arena)
375357
}
376358
} else {
377359
// No child node was rewritten. So no need to change this node as well.

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxVisitorFile.swift

Lines changed: 6 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,8 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3434

3535
DeclSyntax(
3636
"""
37-
/// `Syntax.Info` objects created in `visitChildren` but whose `Syntax` nodes were not retained by the `visit`
38-
/// functions implemented by a subclass of `SyntaxVisitor`.
39-
///
40-
/// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
41-
/// We can then re-use them to create new syntax nodes.
42-
///
43-
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
44-
/// nodes when unwinding the visitation stack.
45-
///
46-
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
47-
/// objects get re-used.
48-
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 64)
49-
"""
50-
)
51-
52-
DeclSyntax(
53-
"""
54-
/// A bit is set to 1 if the corresponding index in `recyclableNodeInfos` is occupied and ready to be reused.
55-
///
56-
/// The last bit in this UInt64 corresponds to index 0 in `recyclableNodeInfos`.
57-
private var recyclableNodeInfosUsageBitmap: UInt64 = 0
37+
/// 'Syntax' object factory recycling 'Syntax.Info' instances.
38+
private let nodeFactory: SyntaxNodeFactory = SyntaxNodeFactory()
5839
"""
5940
)
6041

@@ -261,65 +242,14 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
261242
DeclSyntax(
262243
"""
263244
/// - Note: `node` is `inout` to avoid reference counting. See comment in `visitImpl`.
264-
private func visitChildren(_ syntaxNode: inout Syntax) {
265-
for childRaw in NonNilRawSyntaxChildren(syntaxNode, viewMode: viewMode) {
266-
// syntaxNode gets retained here. That seems unnecessary but I don't know how to remove it.
267-
var childNode: Syntax
268-
if let recycledInfoIndex = recyclableNodeInfosUsageBitmap.indexOfRightmostOne {
269-
var recycledInfo: Syntax.Info? = nil
270-
// Use `swap` to extract the recyclable syntax node without incurring ref-counting.
271-
swap(&recycledInfo, &recyclableNodeInfos[recycledInfoIndex])
272-
assert(recycledInfo != nil, "Slot indicated by the bitmap did not contain a value")
273-
recyclableNodeInfosUsageBitmap.setBitToZero(at: recycledInfoIndex)
274-
// syntaxNode.info gets retained here. This is necessary because we build up the parent tree.
275-
recycledInfo!.info = .nonRoot(.init(parent: syntaxNode, absoluteInfo: childRaw.info))
276-
childNode = Syntax(childRaw.raw, info: recycledInfo!)
277-
} else {
278-
childNode = Syntax(childRaw, parent: syntaxNode)
279-
}
245+
private func visitChildren(_ node: inout Syntax) {
246+
for case let (child?, info) in RawSyntaxChildren(node) where viewMode.shouldTraverse(node: child) {
247+
var childNode = nodeFactory.create(parent: node, raw: child, absoluteInfo: info)
280248
visit(&childNode)
281-
if isKnownUniquelyReferenced(&childNode.info) {
282-
// The node didn't get stored by the subclass's visit method. We can re-use the memory of its `Syntax.Info`
283-
// for future syntax nodes.
284-
childNode.info.info = nil
285-
if let emptySlot = recyclableNodeInfosUsageBitmap.indexOfRightmostZero {
286-
// Use `swap` to store the recyclable syntax node without incurring ref-counting.
287-
swap(&recyclableNodeInfos[emptySlot], &childNode.info)
288-
assert(childNode.info == nil, "Slot should not have contained a value")
289-
recyclableNodeInfosUsageBitmap.setBitToOne(at: emptySlot)
290-
}
291-
}
249+
nodeFactory.dispose(&childNode)
292250
}
293251
}
294252
"""
295253
)
296254
}
297-
298-
DeclSyntax(
299-
"""
300-
fileprivate extension UInt64 {
301-
var indexOfRightmostZero: Int? {
302-
return (~self).indexOfRightmostOne
303-
}
304-
305-
var indexOfRightmostOne: Int? {
306-
let trailingZeroCount = self.trailingZeroBitCount
307-
if trailingZeroCount == Self.bitWidth {
308-
// All indicies are 0
309-
return nil
310-
}
311-
return trailingZeroCount
312-
}
313-
314-
mutating func setBitToZero(at index: Int) {
315-
self &= ~(1 << index)
316-
}
317-
318-
mutating func setBitToOne(at index: Int) {
319-
self |= 1 << index
320-
}
321-
}
322-
323-
"""
324-
)
325255
}

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_swift_syntax_library(SwiftSyntax
3030
SyntaxCollection.swift
3131
SyntaxHashable.swift
3232
SyntaxIdentifier.swift
33+
SyntaxNodeFactory.swift
3334
SyntaxNodeStructure.swift
3435
SyntaxProtocol.swift
3536
SyntaxText.swift

Sources/SwiftSyntax/SyntaxArenaAllocatedBuffer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,8 @@ public struct SyntaxArenaAllocatedBufferPointer<Element: Sendable>: RandomAccess
107107
var unsafeRawBufferPointer: UnsafeRawBufferPointer {
108108
return UnsafeRawBufferPointer(buffer)
109109
}
110+
111+
public func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R? {
112+
try body(buffer)
113+
}
110114
}

0 commit comments

Comments
 (0)