-
Notifications
You must be signed in to change notification settings - Fork 439
[SwiftLexicalLookup] Unqualified lookup caching #3068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e547074
e7c02d8
83e1ca3
047d95b
07aa269
2e8b39f
703d83a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import SwiftSyntax | ||
|
||
/// Unqualified lookup cache. Should be used when performing | ||
/// large sequences of adjacent lookups to maximise performance. | ||
public class LookupCache { | ||
/// Cached results of `ScopeSyntax.lookupParent` calls. | ||
/// Identified by `SyntaxIdentifier`. | ||
private let ancestorResultsCache: LRUCache<SyntaxIdentifier, [LookupResult]> | ||
/// Cached results of `SequentialScopeSyntax.sequentialLookup` calls. | ||
/// Identified by `SyntaxIdentifier`. | ||
private let sequentialResultsCache: LRUCache<SyntaxIdentifier, [LookupResult]> | ||
/// Looked-up scope identifiers during cache accesses. | ||
private var hits: Set<SyntaxIdentifier> = [] | ||
|
||
private let dropMod: Int | ||
private var evictionCount = 0 | ||
|
||
/// Creates a new unqualified lookup cache. | ||
/// `capacity` describes the maximum amount of entries in the cache. | ||
/// The cache size is maintained according to the LRU (Least Recently Used) policy. | ||
/// `drop` parameter specifies how many eviction calls will be | ||
/// ignored before evicting not-hit members from subsequent lookups. | ||
/// | ||
/// Example cache eviction sequences (s - skip, e - evict): | ||
/// - `drop = 0` - `e -> e -> e -> e -> e -> ...` | ||
/// - `drop = 1` - `s -> e -> s -> s -> e -> ...` | ||
/// - `drop = 3` - `s -> s -> s -> e -> s -> ...` | ||
/// | ||
/// - Note: `drop = 0` effectively maintains exactly one path of cached results to | ||
/// the root in the cache (assuming we evict cache members after each lookup in a sequence of lookups). | ||
/// Higher the `drop` value, more such paths can potentially be stored in the cache at any given moment. | ||
/// Because of that, a higher `drop` value also translates to a higher number of cache-hits, | ||
/// but it might not directly translate to better performance. Because of a larger memory footprint, | ||
/// memory accesses could take longer, slowing down the eviction process. That's why the `drop` value | ||
/// could be fine-tuned to maximize the performance given file size, | ||
/// number of lookups, and amount of available memory. | ||
public init(capacity: Int, drop: Int = 0) { | ||
self.ancestorResultsCache = LRUCache(capacity: (capacity + 1) / 2) | ||
self.sequentialResultsCache = LRUCache(capacity: capacity / 2) | ||
self.dropMod = drop + 1 | ||
} | ||
|
||
/// Get cached ancestor results for the given `id`. | ||
/// `nil` if there's no cache entry for the given `id`. | ||
/// Adds `id` and ids of all ancestors to the cache `hits`. | ||
func getCachedAncestorResults(id: SyntaxIdentifier) -> [LookupResult]? { | ||
guard let results = ancestorResultsCache[id] else { return nil } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user doesn’t call |
||
hits.formUnion(results.map(\.scope.id)) | ||
hits.insert(id) | ||
return results | ||
} | ||
|
||
/// Set cached ancestor results for the given `id`. | ||
/// Adds `id` to the cache `hits`. | ||
func setCachedAncestorResults(id: SyntaxIdentifier, results: [LookupResult]) { | ||
hits.insert(id) | ||
ancestorResultsCache[id] = results | ||
} | ||
|
||
/// Get cached sequential lookup results for the given `id`. | ||
/// `nil` if there's no cache entry for the given `id`. | ||
/// Adds `id` to the cache `hits`. | ||
func getCachedSequentialResults(id: SyntaxIdentifier) -> [LookupResult]? { | ||
guard let results = sequentialResultsCache[id] else { return nil } | ||
hits.insert(id) | ||
return results | ||
} | ||
|
||
/// Set cached sequential lookup results for the given `id`. | ||
/// Adds `id` to the cache `hits`. | ||
func setCachedSequentialResults(id: SyntaxIdentifier, results: [LookupResult]) { | ||
hits.insert(id) | ||
sequentialResultsCache[id] = results | ||
} | ||
|
||
/// Removes all cached entries without a hit, unless it's prohibited | ||
/// by the internal drop counter (as specified by `drop` in the initializer). | ||
/// The dropping behavior can be disabled for this call with the `bypassDropCounter` | ||
/// parameter, resulting in immediate eviction of entries without a hit. | ||
public func evictEntriesWithoutHit(bypassDropCounter: Bool = false) { | ||
if !bypassDropCounter { | ||
evictionCount = (evictionCount + 1) % dropMod | ||
guard evictionCount != 0 else { return } | ||
} | ||
|
||
for key in ancestorResultsCache.keysInCache.union(sequentialResultsCache.keysInCache).subtracting(hits) { | ||
ancestorResultsCache[key] = nil | ||
sequentialResultsCache[key] = nil | ||
} | ||
Comment on lines
+98
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how performance sensitive this is or how expensive the for key in ancestorResultsCache where !hits.contains(key) {
ancestorResultsCache[key] = nil
}
for key in sequentialResultsCache where !hits.contains(key) {
sequentialResultsCache[key] = nil
} |
||
|
||
hits = [] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an idea: Would it make sense to move the
drop
parameter toevictEntriesWithoutHit
. That way clients don’t have to think about the dropping cache eviction policy unless they start callingevictEntriesWithoutHit
. It would also open up the option to vary the size of the cache dynamically depending on the shape of the code that we’re in (not sure if that’s useful or not). It would also remove the need forbypassDropCounter
in that function because you could passdrop: 0
there, I think.