Skip to content

Commit 5d4b04d

Browse files
authored
[6.0] Performance improvements for SyntaxRewriter (#2742)
* [SyntaxRewriter] Optimize SyntaxRewriter visitation Similar treatment as 'SyntaxVisitor'. Reuse `Syntax.Info` when it's safe to do so (i.e. uniquely referenced) (cherry picked from commit 6be8e8d) * [SyntaxRewriter] Introduce SyntaxNodeFactory Factor out 'Syntax.Info' reusing into 'SyntaxNodeFactory' and 'SyntaxInfoRepository' as the backing storage. The underlying storage now uses a fixed length 'ManagedBuffer' to avoid 'ContiguousArray' overhead. (cherry picked from commit b6898d1) * [SyntaxRewriter] Improve new layout node creation Use manually allocated 'UnsafeBufferPointer' to avoid 'ContiguousArray' overhead. Pre-initialize the buffer with the "old" layout at once, and update each element only when updated. (cherry picked from commit e664473) * [SyntaxRewriter] Make 'visitChildren()' a non-generic mehod This was a generic function only for casting the result at the end. Hoist the casting part to the caller and make 'visitChilden()' non-generic. (cherry picked from commit e2d4423) * [SyntaxRewriter] Iterate RawSyntaxChildren using pattern matching Simpify the iteration code. Also, stop counting 'childIndex' as it can be retrieved from the absolute info. (cherry picked from commit b67e899) * [SyntaxVisitor] Adopt SyntaxNodeFactory Use 'SyntaxNodeFactory' in 'SyntaxVisitor' too. Thanks to the simplified implementation, it improves the performance a bit. (cherry picked from commit 7298fe8) * [SyntaxRewriter] Return the node as-is There's no performance reason to return 'nil' (cherry picked from commit ef8bb05) * [CMake] Split 'touch' workaround into two commands Workaround for Windows command line length limitation (cherry picked from commit ec96b4d) * [Syntax] Mark the parameter of casting initializers '__shared' Initializer parameters are "+1" in caller site, but these casting initializers only uses the ._syntaxNode and don't store the parameter itself. (cherry picked from commit 99b670f) * [Syntax] Mark parameter of Syntax.init(_:) casting function Similar to `SyntaxProtocol.init()` initializers. (cherry picked from commit c5526ac) * [SyntaxNode] Make 'static var structure' immutable stored properties Returnning array literal from computed property allocates and initializes the array buffer every time it's called, and they are deallocated after use. Make them stored properties so they stay in memory. (cherry picked from commit 92dbbf8) * [Operators] Make OpeatorTable.standardOperators a stored property Building standard operator table is not trivial. Make is an immutable stored property so it stays in memory once it's initialized. (cherry picked from commit fad2743)
1 parent ed23a17 commit 5d4b04d

24 files changed

+3465
-4004
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
183183
DeclSyntax(
184184
"""
185185
/// Create a \(raw: node.kind.doccLink) node from a specialized syntax node.
186-
public init(_ syntax: some \(node.kind.protocolType)) {
186+
public init(_ syntax: __shared some \(node.kind.protocolType)) {
187187
// We know this cast is going to succeed. Go through init(_: SyntaxData)
188188
// to do a sanity check and verify the kind matches in debug builds and get
189189
// maximum performance in release builds.
@@ -195,7 +195,7 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
195195
DeclSyntax(
196196
"""
197197
/// Create a \(raw: node.kind.doccLink) node from a specialized optional syntax node.
198-
public init?(_ syntax: (some \(node.kind.protocolType))?) {
198+
public init?(_ syntax: __shared (some \(node.kind.protocolType))?) {
199199
guard let syntax = syntax else { return nil }
200200
self.init(syntax)
201201
}
@@ -204,7 +204,7 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
204204

205205
DeclSyntax(
206206
"""
207-
public init(fromProtocol syntax: \(node.kind.protocolType)) {
207+
public init(fromProtocol syntax: __shared \(node.kind.protocolType)) {
208208
// We know this cast is going to succeed. Go through init(_: SyntaxData)
209209
// to do a sanity check and verify the kind matches in debug builds and get
210210
// maximum performance in release builds.
@@ -216,14 +216,14 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
216216
DeclSyntax(
217217
"""
218218
/// Create a \(raw: node.kind.doccLink) node from a specialized optional syntax node.
219-
public init?(fromProtocol syntax: \(node.kind.protocolType)?) {
219+
public init?(fromProtocol syntax: __shared \(node.kind.protocolType)?) {
220220
guard let syntax = syntax else { return nil }
221221
self.init(fromProtocol: syntax)
222222
}
223223
"""
224224
)
225225

226-
try InitializerDeclSyntax("public init?(_ node: some SyntaxProtocol)") {
226+
try InitializerDeclSyntax("public init?(_ node: __shared some SyntaxProtocol)") {
227227
try SwitchExprSyntax("switch node.raw.kind") {
228228
SwitchCaseListSyntax {
229229
SwitchCaseSyntax(

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
4747

4848
DeclSyntax(
4949
"""
50-
public init?(_ node: some SyntaxProtocol) {
50+
public init?(_ node: __shared some SyntaxProtocol) {
5151
guard node.raw.kind == .\(node.varOrCaseName) else { return nil }
5252
self._syntaxNode = node._syntaxNode
5353
}
@@ -199,17 +199,14 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
199199
}
200200
}
201201

202-
try! VariableDeclSyntax("public static var structure: SyntaxNodeStructure") {
203-
let layout = ArrayExprSyntax {
204-
for child in node.children {
205-
ArrayElementSyntax(
206-
expression: ExprSyntax(#"\Self.\#(child.varOrCaseName)"#)
207-
)
208-
}
202+
let layout = ArrayExprSyntax {
203+
for child in node.children {
204+
ArrayElementSyntax(
205+
expression: ExprSyntax(#"\Self.\#(child.varOrCaseName)"#)
206+
)
209207
}
210-
211-
StmtSyntax("return .layout(\(layout))")
212208
}
209+
"public static let structure: SyntaxNodeStructure = .layout(\(layout))"
213210
}
214211
}
215212
}
@@ -256,7 +253,7 @@ private func generateSyntaxChildChoices(for child: Child) throws -> EnumDeclSynt
256253
}
257254
}
258255

259-
try! InitializerDeclSyntax("public init?(_ node: some SyntaxProtocol)") {
256+
try! InitializerDeclSyntax("public init?(_ node: __shared some SyntaxProtocol)") {
260257
for choice in choices {
261258
StmtSyntax(
262259
"""

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

Lines changed: 61 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3232
) {
3333
DeclSyntax("public let viewMode: SyntaxTreeViewMode")
3434

35+
DeclSyntax(
36+
"""
37+
/// 'Syntax' object factory recycling 'Syntax.Info' instances.
38+
private let nodeFactory: SyntaxNodeFactory = SyntaxNodeFactory()
39+
"""
40+
)
41+
3542
DeclSyntax(
3643
"""
3744
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
@@ -44,7 +51,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4451
"""
4552
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
4653
public func rewrite(_ node: some SyntaxProtocol, detach: Bool = false) -> Syntax {
47-
let rewritten = self.dispatchVisit(Syntax(node))
54+
var rewritten = Syntax(node)
55+
self.dispatchVisit(&rewritten)
4856
if detach {
4957
return rewritten
5058
}
@@ -105,15 +113,19 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
105113
/// - Returns: the rewritten node
106114
@available(*, deprecated, renamed: "rewrite(_:detach:)")
107115
public func visit(_ node: Syntax) -> Syntax {
108-
return dispatchVisit(node)
116+
var rewritten = node
117+
dispatchVisit(&rewritten)
118+
return rewritten
109119
}
110120
"""
111121
)
112122

113123
DeclSyntax(
114124
"""
115125
public func visit<T: SyntaxChildChoices>(_ node: T) -> T {
116-
return dispatchVisit(Syntax(node)).cast(T.self)
126+
var rewritten = Syntax(node)
127+
dispatchVisit(&rewritten)
128+
return rewritten.cast(T.self)
117129
}
118130
"""
119131
)
@@ -127,7 +139,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
127139
/// - Returns: the rewritten node
128140
\(node.apiAttributes())\
129141
open func visit(_ node: \(node.kind.syntaxType)) -> \(node.kind.syntaxType) {
130-
return visitChildren(node)
142+
return visitChildren(node._syntaxNode).cast(\(node.kind.syntaxType).self)
131143
}
132144
"""
133145
)
@@ -139,7 +151,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
139151
/// - Returns: the rewritten node
140152
\(node.apiAttributes())\
141153
open func visit(_ node: \(node.kind.syntaxType)) -> \(node.baseType.syntaxBaseName) {
142-
return \(node.baseType.syntaxBaseName)(visitChildren(node))
154+
return \(node.baseType.syntaxBaseName)(visitChildren(node._syntaxNode).cast(\(node.kind.syntaxType).self))
143155
}
144156
"""
145157
)
@@ -156,7 +168,9 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
156168
/// - Returns: the rewritten node
157169
\(baseNode.apiAttributes())\
158170
public func visit(_ node: \(baseKind.syntaxType)) -> \(baseKind.syntaxType) {
159-
return dispatchVisit(Syntax(node)).cast(\(baseKind.syntaxType).self)
171+
var node: Syntax = Syntax(node)
172+
dispatchVisit(&node)
173+
return node.cast(\(baseKind.syntaxType).self)
160174
}
161175
"""
162176
)
@@ -166,21 +180,16 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
166180
"""
167181
/// Interpret `node` as a node of type `nodeType`, visit it, calling
168182
/// the `visit` to transform the node.
183+
@inline(__always)
169184
private func visitImpl<NodeType: SyntaxProtocol>(
170-
_ node: Syntax,
185+
_ node: inout Syntax,
171186
_ nodeType: NodeType.Type,
172187
_ visit: (NodeType) -> some SyntaxProtocol
173-
) -> Syntax {
174-
let castedNode = node.cast(NodeType.self)
175-
// Accessing _syntaxNode directly is faster than calling Syntax(node)
176-
visitPre(node)
177-
defer {
178-
visitPost(node)
179-
}
180-
if let newNode = visitAny(node) {
181-
return newNode
182-
}
183-
return Syntax(visit(castedNode))
188+
) {
189+
let origNode = node
190+
visitPre(origNode)
191+
node = visitAny(origNode) ?? Syntax(visit(origNode.cast(NodeType.self)))
192+
visitPost(origNode)
184193
}
185194
"""
186195
)
@@ -221,26 +230,26 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
221230
/// that determines the correct visitation function will be popped of the
222231
/// stack before the function is being called, making the switch's stack
223232
/// space transient instead of having it linger in the call stack.
224-
private func visitationFunc(for node: Syntax) -> ((Syntax) -> Syntax)
233+
private func visitationFunc(for node: Syntax) -> ((inout Syntax) -> Void)
225234
"""
226235
) {
227236
try SwitchExprSyntax("switch node.raw.kind") {
228237
SwitchCaseSyntax("case .token:") {
229-
StmtSyntax("return { self.visitImpl($0, TokenSyntax.self, self.visit) }")
238+
StmtSyntax("return { self.visitImpl(&$0, TokenSyntax.self, self.visit) }")
230239
}
231240

232241
for node in NON_BASE_SYNTAX_NODES {
233242
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
234-
StmtSyntax("return { self.visitImpl($0, \(node.kind.syntaxType).self, self.visit) }")
243+
StmtSyntax("return { self.visitImpl(&$0, \(node.kind.syntaxType).self, self.visit) }")
235244
}
236245
}
237246
}
238247
}
239248

240249
DeclSyntax(
241250
"""
242-
private func dispatchVisit(_ node: Syntax) -> Syntax {
243-
return visitationFunc(for: node)(node)
251+
private func dispatchVisit(_ node: inout Syntax) {
252+
visitationFunc(for: node)(&node)
244253
}
245254
"""
246255
)
@@ -251,15 +260,15 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
251260
poundKeyword: .poundElseToken(),
252261
elements: .statements(
253262
CodeBlockItemListSyntax {
254-
try! FunctionDeclSyntax("private func dispatchVisit(_ node: Syntax) -> Syntax") {
263+
try! FunctionDeclSyntax("private func dispatchVisit(_ node: inout Syntax)") {
255264
try SwitchExprSyntax("switch node.raw.kind") {
256265
SwitchCaseSyntax("case .token:") {
257-
StmtSyntax("return visitImpl(node, TokenSyntax.self, visit)")
266+
StmtSyntax("return visitImpl(&node, TokenSyntax.self, visit)")
258267
}
259268

260269
for node in NON_BASE_SYNTAX_NODES {
261270
SwitchCaseSyntax("case .\(node.varOrCaseName):") {
262-
StmtSyntax("return visitImpl(node, \(node.kind.syntaxType).self, visit)")
271+
StmtSyntax("return visitImpl(&node, \(node.kind.syntaxType).self, visit)")
263272
}
264273
}
265274
}
@@ -272,9 +281,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
272281

273282
DeclSyntax(
274283
"""
275-
private func visitChildren<SyntaxType: SyntaxProtocol>(
276-
_ node: SyntaxType
277-
) -> SyntaxType {
284+
private func visitChildren(_ node: Syntax) -> Syntax {
278285
// Walk over all children of this node and rewrite them. Don't store any
279286
// rewritten nodes until the first non-`nil` value is encountered. When this
280287
// happens, retrieve all previous syntax nodes from the parent node to
@@ -284,73 +291,48 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
284291
285292
// newLayout is nil until the first child node is rewritten and rewritten
286293
// nodes are being collected.
287-
var newLayout: ContiguousArray<RawSyntax?>?
288-
289-
// Rewritten children just to keep their 'SyntaxArena' alive until they are
290-
// wrapped with 'Syntax'
291-
var rewrittens: ContiguousArray<Syntax> = []
294+
var newLayout: UnsafeMutableBufferPointer<RawSyntax?> = .init(start: nil, count: 0)
292295
293-
let syntaxNode = node._syntaxNode
296+
// Keep 'SyntaxArena' of rewritten nodes alive until they are wrapped
297+
// with 'Syntax'
298+
var rewrittens: ContiguousArray<RetainedSyntaxArena> = []
294299
295-
// Incrementing i manually is faster than using .enumerated()
296-
var childIndex = 0
297-
for (raw, info) in RawSyntaxChildren(syntaxNode) {
298-
defer { childIndex += 1 }
299-
300-
guard let child = raw, viewMode.shouldTraverse(node: child) else {
301-
// Node does not exist or should not be visited. If we are collecting
302-
// rewritten nodes, we need to collect this one as well, otherwise we
303-
// can ignore it.
304-
if newLayout != nil {
305-
newLayout!.append(raw)
306-
}
307-
continue
308-
}
300+
for case let (child?, info) in RawSyntaxChildren(node) where viewMode.shouldTraverse(node: child) {
309301
310302
// Build the Syntax node to rewrite
311-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
303+
var childNode = nodeFactory.create(parent: node, raw: child, absoluteInfo: info)
312304
313-
let rewritten = dispatchVisit(Syntax(absoluteRaw, parent: syntaxNode))
314-
if rewritten.id != info.nodeId {
305+
dispatchVisit(&childNode)
306+
if childNode.raw.id != child.id {
315307
// The node was rewritten, let's handle it
316-
if newLayout == nil {
308+
309+
if newLayout.baseAddress == nil {
317310
// We have not yet collected any previous rewritten nodes. Initialize
318-
// the new layout with the previous nodes of the parent. This is
319-
// possible, since we know they were not rewritten.
320-
321-
// The below implementation is based on Collection.map but directly
322-
// reserves enough capacity for the entire layout.
323-
newLayout = ContiguousArray<RawSyntax?>()
324-
newLayout!.reserveCapacity(node.raw.layoutView!.children.count)
325-
for j in 0..<childIndex {
326-
newLayout!.append(node.raw.layoutView!.children[j])
327-
}
311+
// the new layout with the previous nodes of the parent.
312+
newLayout = .allocate(capacity: node.raw.layoutView!.children.count)
313+
_ = newLayout.initialize(fromContentsOf: node.raw.layoutView!.children)
328314
}
329315
330-
// Now that we know we have a new layout in which we collect rewritten
331-
// nodes, add it.
332-
rewrittens.append(rewritten)
333-
newLayout!.append(rewritten.raw)
334-
} else {
335-
// The node was not changed by the rewriter. Only store it if a previous
336-
// node has been rewritten and we are collecting a rewritten layout.
337-
if newLayout != nil {
338-
newLayout!.append(raw)
339-
}
316+
// Update the rewritten child.
317+
newLayout[Int(info.indexInParent)] = childNode.raw
318+
// Retain the syntax arena of the new node until it's wrapped with Syntax node.
319+
rewrittens.append(childNode.raw.arenaReference.retained)
340320
}
321+
322+
// Recycle 'childNode.info'
323+
nodeFactory.dispose(&childNode)
341324
}
342325
343-
if let newLayout {
326+
if newLayout.baseAddress != nil {
344327
// A child node was rewritten. Build the updated node.
345328
346-
// Sanity check, ensure the new children are the same length.
347-
precondition(newLayout.count == node.raw.layoutView!.children.count)
348-
349329
let arena = SyntaxArena()
350-
let newRaw = node.raw.layoutView!.replacingLayout(with: Array(newLayout), arena: arena)
330+
let newRaw = node.raw.layoutView!.replacingLayout(with: newLayout, arena: arena)
331+
newLayout.deinitialize()
332+
newLayout.deallocate()
351333
// 'withExtendedLifetime' to keep 'SyntaxArena's of them alive until here.
352334
return withExtendedLifetime(rewrittens) {
353-
Syntax(raw: newRaw, rawNodeArena: arena).cast(SyntaxType.self)
335+
Syntax(raw: newRaw, rawNodeArena: arena)
354336
}
355337
} else {
356338
// No child node was rewritten. So no need to change this node as well.

0 commit comments

Comments
 (0)