Skip to content

Commit 205cf95

Browse files
committed
Use a bitmap to store which slots in recyclableNodeInfos are free
This gets us another ~3% performance improvement in SwiftLint listing time.
1 parent b892dda commit 205cf95

File tree

2 files changed

+84
-12
lines changed

2 files changed

+84
-12
lines changed

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4141
/// We can then re-use them to create new syntax nodes.
4242
///
4343
/// 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. It shouldn't be much larger because that would mean that we need to
45-
/// look through more memory to find a cache miss. 40 has been chosen empirically to strike a good balance here.
44+
/// nodes when unwinding the visitation stack.
4645
///
4746
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
4847
/// objects get re-used.
49-
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 40)
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
5058
"""
5159
)
5260

@@ -255,10 +263,12 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
255263
for childRaw in NonNilRawSyntaxChildren(syntaxNode, viewMode: viewMode) {
256264
// syntaxNode gets retained here. That seems unnecessary but I don't know how to remove it.
257265
var childNode: Syntax
258-
if let recycledInfoIndex = recyclableNodeInfos.firstIndex(where: { $0 != nil }) {
266+
if let recycledInfoIndex = recyclableNodeInfosUsageBitmap.indexOfRightmostOne {
259267
var recycledInfo: Syntax.Info? = nil
260268
// Use `swap` to extract the recyclable syntax node without incurring ref-counting.
261269
swap(&recycledInfo, &recyclableNodeInfos[recycledInfoIndex])
270+
assert(recycledInfo != nil, "Slot indicated by the bitmap did not contain a value")
271+
recyclableNodeInfosUsageBitmap.setBitToZero(at: recycledInfoIndex)
262272
// syntaxNode.info gets retained here. This is necessary because we build up the parent tree.
263273
recycledInfo!.info = .nonRoot(.init(parent: syntaxNode, absoluteInfo: childRaw.info))
264274
childNode = Syntax(childRaw.raw, info: recycledInfo!)
@@ -270,14 +280,44 @@ let syntaxVisitorFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
270280
// The node didn't get stored by the subclass's visit method. We can re-use the memory of its `Syntax.Info`
271281
// for future syntax nodes.
272282
childNode.info.info = nil
273-
if let emptySlot = recyclableNodeInfos.firstIndex(where: { $0 == nil }) {
283+
if let emptySlot = recyclableNodeInfosUsageBitmap.indexOfRightmostZero {
274284
// Use `swap` to store the recyclable syntax node without incurring ref-counting.
275285
swap(&recyclableNodeInfos[emptySlot], &childNode.info)
286+
assert(childNode.info == nil, "Slot should not have contained a value")
287+
recyclableNodeInfosUsageBitmap.setBitToOne(at: emptySlot)
276288
}
277289
}
278290
}
279291
}
280292
"""
281293
)
282294
}
295+
296+
DeclSyntax(
297+
"""
298+
fileprivate extension UInt64 {
299+
var indexOfRightmostZero: Int? {
300+
return (~self).indexOfRightmostOne
301+
}
302+
303+
var indexOfRightmostOne: Int? {
304+
let trailingZeroCount = self.trailingZeroBitCount
305+
if trailingZeroCount == Self.bitWidth {
306+
// All indicies are 0
307+
return nil
308+
}
309+
return trailingZeroCount
310+
}
311+
312+
mutating func setBitToZero(at index: Int) {
313+
self &= ~(1 << index)
314+
}
315+
316+
mutating func setBitToOne(at index: Int) {
317+
self |= 1 << index
318+
}
319+
}
320+
321+
"""
322+
)
283323
}

Sources/SwiftSyntax/generated/SyntaxVisitor.swift

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ open class SyntaxVisitor {
3131
/// We can then re-use them to create new syntax nodes.
3232
///
3333
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
34-
/// nodes when unwinding the visitation stack. It shouldn't be much larger because that would mean that we need to
35-
/// look through more memory to find a cache miss. 40 has been chosen empirically to strike a good balance here.
34+
/// nodes when unwinding the visitation stack.
3635
///
3736
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
3837
/// objects get re-used.
39-
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 40)
38+
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?> = ContiguousArray(repeating: nil, count: 64)
39+
40+
/// A bit is set to 1 if the corresponding index in `recyclableNodeInfos` is occupied and ready to be reused.
41+
///
42+
/// The last bit in this UInt64 corresponds to index 0 in `recyclableNodeInfos`.
43+
private var recyclableNodeInfosUsageBitmap: UInt64 = 0
4044

4145
public init(viewMode: SyntaxTreeViewMode) {
4246
self.viewMode = viewMode
@@ -5291,11 +5295,12 @@ open class SyntaxVisitor {
52915295
for childRaw in NonNilRawSyntaxChildren(syntaxNode, viewMode: viewMode) {
52925296
// syntaxNode gets retained here. That seems unnecessary but I don't know how to remove it.
52935297
var childNode: Syntax
5294-
if let recycledInfoIndex = recyclableNodeInfos.firstIndex(where: { $0 != nil
5295-
}) {
5298+
if let recycledInfoIndex = recyclableNodeInfosUsageBitmap.indexOfRightmostOne {
52965299
var recycledInfo: Syntax.Info? = nil
52975300
// Use `swap` to extract the recyclable syntax node without incurring ref-counting.
52985301
swap(&recycledInfo, &recyclableNodeInfos[recycledInfoIndex])
5302+
assert(recycledInfo != nil, "Slot indicated by the bitmap did not contain a value")
5303+
recyclableNodeInfosUsageBitmap.setBitToZero(at: recycledInfoIndex)
52995304
// syntaxNode.info gets retained here. This is necessary because we build up the parent tree.
53005305
recycledInfo!.info = .nonRoot(.init(parent: syntaxNode, absoluteInfo: childRaw.info))
53015306
childNode = Syntax(childRaw.raw, info: recycledInfo!)
@@ -5307,12 +5312,39 @@ open class SyntaxVisitor {
53075312
// The node didn't get stored by the subclass's visit method. We can re-use the memory of its `Syntax.Info`
53085313
// for future syntax nodes.
53095314
childNode.info.info = nil
5310-
if let emptySlot = recyclableNodeInfos.firstIndex(where: { $0 == nil
5311-
}) {
5315+
if let emptySlot = recyclableNodeInfosUsageBitmap.indexOfRightmostZero {
53125316
// Use `swap` to store the recyclable syntax node without incurring ref-counting.
53135317
swap(&recyclableNodeInfos[emptySlot], &childNode.info)
5318+
assert(childNode.info == nil, "Slot should not have contained a value")
5319+
recyclableNodeInfosUsageBitmap.setBitToOne(at: emptySlot)
53145320
}
53155321
}
53165322
}
53175323
}
53185324
}
5325+
5326+
fileprivate extension UInt64 {
5327+
var indexOfRightmostZero: Int? {
5328+
return (~self).indexOfRightmostOne
5329+
}
5330+
5331+
5332+
var indexOfRightmostOne: Int? {
5333+
let trailingZeroCount = self.trailingZeroBitCount
5334+
if trailingZeroCount == Self.bitWidth {
5335+
// All indicies are 0
5336+
return nil
5337+
}
5338+
return trailingZeroCount
5339+
}
5340+
5341+
5342+
mutating func setBitToZero(at index: Int) {
5343+
self &= ~(1 << index)
5344+
}
5345+
5346+
5347+
mutating func setBitToOne(at index: Int) {
5348+
self |= 1 << index
5349+
}
5350+
}

0 commit comments

Comments
 (0)