-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Simplifying internal structure and logic for the SyntaxNodeCache #80825
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
Changes from 4 commits
efce2b2
f95e863
4d5d748
8cd1520
77ebcde
5d12590
5f04d74
d4989bb
411b229
6bc244b
891a8cb
f4e8121
40c5796
c14525a
ac3fa11
30a6a9f
8ffd9dd
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 |
|---|---|---|
|
|
@@ -2,10 +2,9 @@ | |
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using Microsoft.CodeAnalysis.Syntax.InternalSyntax; | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Runtime.CompilerServices; | ||
| using Roslyn.Utilities; | ||
|
|
||
| #if STATS | ||
|
|
@@ -15,6 +14,7 @@ namespace Microsoft.CodeAnalysis.Syntax.InternalSyntax | |
| { | ||
| /// <summary> | ||
| /// Provides caching functionality for green nonterminals with up to 3 children. | ||
| /// | ||
|
CyrusNajmabadi marked this conversation as resolved.
Outdated
|
||
| /// Example: | ||
| /// When constructing a node with given kind, flags, child1 and child2, we can look up | ||
| /// in the cache whether we already have a node that contains same kind, flags, | ||
|
|
@@ -38,8 +38,21 @@ namespace Microsoft.CodeAnalysis.Syntax.InternalSyntax | |
| /// We only consider "normal" nodes to be cacheable. | ||
| /// Nodes with diagnostics/annotations/directives/skipped, etc... have more complicated identity | ||
| /// and are not likely to be repetitive. | ||
| /// | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The use of <see cref="GreenNode.RawKind"/>, <see cref="GreenNode.NodeFlags"/> (and any provided child nodes) | ||
| /// ensures during lookup that we only return a node that is identical in all relevant aspects to the node that | ||
| /// we're about to create otherwise. This is a brittle guarantee. However, given how locked down green nodes are, | ||
| /// this seems acceptable for now. Great care needs to be taken if new properties are added to green nodes that | ||
| /// would affect their identity. | ||
| /// <para/> | ||
| /// Only nodes created through SyntaxFactory methods are cached. Nodes created directly through their constructors | ||
| /// are not cached. This is fairly intuitive as a constructor would not be able to somehow return some other | ||
| /// instance different than the one being constructed. A subtle aspect of this though is that this is what ensures | ||
| /// that nodes with diagnostics or annotations on them are not cached. These nodes are created starting with | ||
| /// another node and forking it to add diagnostics/annotations. This forking always calls through a constructor and | ||
| /// not a factory method. And as such, never comes through here. | ||
| /// </remarks> | ||
| internal class GreenStats | ||
| { | ||
| // TODO: remove when done tweaking this cache. | ||
|
|
@@ -114,36 +127,31 @@ internal static class SyntaxNodeCache | |
| private const int CacheSize = 1 << CacheSizeBits; | ||
| private const int CacheMask = CacheSize - 1; | ||
|
|
||
| private readonly struct Entry | ||
| { | ||
| public readonly int hash; | ||
| public readonly GreenNode? node; | ||
|
|
||
| internal Entry(int hash, GreenNode node) | ||
| { | ||
| this.hash = hash; | ||
| this.node = node; | ||
| } | ||
| } | ||
|
Contributor
Author
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. This type was not helpful. First, it made it so that one had to reason about the semantics of torn reads/writes in the array below. The semantics were correct, but were very subtle and difficult to prove. Second, extra data stored here did not help at all. The hash that was read could not actually be asserted to be anything (since collisions overwrite). And the hash could only be used, at best, to no effect to compare against the existing hash into that location. But at worse, it could disallow reuse of a node that could be reused during a tear. The hash itself is computed from data that is already checked in IsNodeEquivalent. While technically this might reduce the number of checks performed when there was a collision, practically, this turns out to almost always be one check max as the Kind validation almost always immediately fails. See PR op for more data on this. Note: the Kind/Flags checks are non-virtual and are just plucking raw data out of the green node. So we're not adding any indirections or anything like that. |
||
|
|
||
| private static readonly Entry[] s_cache = new Entry[CacheSize]; | ||
| /// <summary> | ||
| /// Simply array indexed by the hash of the cached node. Note that unlike a typical dictionary/hashtable, this | ||
|
CyrusNajmabadi marked this conversation as resolved.
Outdated
|
||
| /// does not exercise any form of collision resolution. If two different nodes hash to the same index, the | ||
| /// latter will overwrite the former. This is acceptable since this is just an opportunistic cache. Reads from | ||
| /// the cache validate that the node they get back is actually the one they were looking for. See the comments | ||
| /// in <see cref="TryGetNode(int, GreenNode?, out int)"/> for more details. | ||
| /// </summary> | ||
| private static readonly GreenNode[] s_cache = new GreenNode[CacheSize]; | ||
|
|
||
| internal static void AddNode(GreenNode node, int hash) | ||
| { | ||
| if (AllChildrenInCache(node) && !node.IsMissing) | ||
| { | ||
| GreenStats.ItemAdded(); | ||
|
|
||
| Debug.Assert(node.GetCacheHash() == hash); | ||
| Debug.Assert(GetCacheHash(node) == hash); | ||
|
|
||
| var idx = hash & CacheMask; | ||
| s_cache[idx] = new Entry(hash, node); | ||
| s_cache[idx] = node; | ||
| } | ||
| } | ||
|
|
||
| private static bool CanBeCached(GreenNode? child1) | ||
| { | ||
| return child1 == null || child1.IsCacheable; | ||
| return child1 == null || IsCacheable(child1); | ||
| } | ||
|
|
||
| private static bool CanBeCached(GreenNode? child1, GreenNode? child2) | ||
|
|
@@ -163,9 +171,9 @@ private static bool ChildInCache(GreenNode? child) | |
| // TODO: should use slotCount | ||
| if (child == null || child.SlotCount == 0) return true; | ||
|
|
||
| int hash = child.GetCacheHash(); | ||
| int hash = GetCacheHash(child); | ||
| int idx = hash & CacheMask; | ||
|
333fred marked this conversation as resolved.
|
||
| return s_cache[idx].node == child; | ||
| return s_cache[idx] == child; | ||
| } | ||
|
|
||
| private static bool AllChildrenInCache(GreenNode node) | ||
|
|
@@ -194,13 +202,18 @@ private static bool AllChildrenInCache(GreenNode node) | |
| { | ||
| GreenStats.ItemCacheable(); | ||
|
|
||
| // Determine the hash for the node being created, given its kind, flags, and optional single child. Then | ||
| // grab out a potential cached node from the cache based on that hash. Note that this may not actually | ||
| // be a viable match due to potential hash collisions, where we have 'last one wins' semantics. So if | ||
| // we do see a node in the cache, we have to validate that it is actually equivalent to the the data | ||
|
CyrusNajmabadi marked this conversation as resolved.
Outdated
|
||
| // being used populate the cache entry. This is what IsCacheEquivalent is for. It allows us to check | ||
|
CyrusNajmabadi marked this conversation as resolved.
Outdated
|
||
| // that the node already there has that same kind, flags, and the same child (by reference). | ||
| int h = hash = GetCacheHash(kind, flags, child1); | ||
| int idx = h & CacheMask; | ||
| var e = s_cache[idx]; | ||
| if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1)) | ||
| var e = s_cache[h & CacheMask]; | ||
| if (IsCacheEquivalent(e, kind, flags, child1)) | ||
| { | ||
| GreenStats.CacheHit(); | ||
| return e.node; | ||
| return e; | ||
| } | ||
| } | ||
| else | ||
|
|
@@ -223,12 +236,11 @@ private static bool AllChildrenInCache(GreenNode node) | |
| GreenStats.ItemCacheable(); | ||
|
|
||
| int h = hash = GetCacheHash(kind, flags, child1, child2); | ||
| int idx = h & CacheMask; | ||
| var e = s_cache[idx]; | ||
| if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1, child2)) | ||
| var e = s_cache[h & CacheMask]; | ||
| if (IsCacheEquivalent(e, kind, flags, child1, child2)) | ||
| { | ||
| GreenStats.CacheHit(); | ||
| return e.node; | ||
| return e; | ||
| } | ||
| } | ||
| else | ||
|
|
@@ -251,12 +263,11 @@ private static bool AllChildrenInCache(GreenNode node) | |
| GreenStats.ItemCacheable(); | ||
|
|
||
| int h = hash = GetCacheHash(kind, flags, child1, child2, child3); | ||
| int idx = h & CacheMask; | ||
| var e = s_cache[idx]; | ||
| if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1, child2, child3)) | ||
| var e = s_cache[h & CacheMask]; | ||
| if (IsCacheEquivalent(e, kind, flags, child1, child2, child3)) | ||
| { | ||
| GreenStats.CacheHit(); | ||
| return e.node; | ||
| return e; | ||
| } | ||
| } | ||
| else | ||
|
|
@@ -318,5 +329,82 @@ private static int GetCacheHash(int kind, GreenNode.NodeFlags flags, GreenNode? | |
| // ensure nonnegative hash | ||
| return code & Int32.MaxValue; | ||
| } | ||
|
|
||
| private const int MaxCachedChildNum = 3; | ||
|
|
||
| private static bool IsCacheable(GreenNode node) | ||
| { | ||
| return ((node.Flags & GreenNode.NodeFlags.InheritMask) == GreenNode.NodeFlags.IsNotMissing) && | ||
| node.SlotCount <= MaxCachedChildNum; | ||
| } | ||
|
|
||
| private static int GetCacheHash(GreenNode node) | ||
| { | ||
| Debug.Assert(IsCacheable(node)); | ||
|
|
||
| int code = (int)(node.Flags) ^ node.RawKind; | ||
| int cnt = node.SlotCount; | ||
| for (int i = 0; i < cnt; i++) | ||
| { | ||
| var child = node.GetSlot(i); | ||
| if (child != null) | ||
| { | ||
| code = Hash.Combine(RuntimeHelpers.GetHashCode(child), code); | ||
| } | ||
| } | ||
|
|
||
| return code & Int32.MaxValue; | ||
| } | ||
|
|
||
| private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1) | ||
| { | ||
| if (parent is null) | ||
| return false; | ||
|
|
||
| Debug.Assert(IsCacheable(parent)); | ||
|
|
||
| // If the parent kind matches, the slot count must match as well. Only if the parent kind does not match | ||
| // (so two different node kinds hashed to the same array location) could the slot counts differ. | ||
| Debug.Assert(parent.RawKind != kind || parent.SlotCount == 1); | ||
|
|
||
| return parent.RawKind == kind && | ||
| parent.Flags == flags && | ||
| parent.GetSlot(0) == child1; | ||
| } | ||
|
|
||
| private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1, GreenNode? child2) | ||
| { | ||
| if (parent is null) | ||
| return false; | ||
|
|
||
| Debug.Assert(IsCacheable(parent)); | ||
|
|
||
| // If the parent kind matches, the slot count must match as well. Only if the parent kind does not match | ||
| // (so two different node kinds hashed to the same array location) could the slot counts differ. | ||
| Debug.Assert(parent.RawKind != kind || parent.SlotCount == 2); | ||
|
|
||
| return parent.RawKind == kind && | ||
| parent.Flags == flags && | ||
| parent.GetSlot(0) == child1 && | ||
| parent.GetSlot(1) == child2; | ||
| } | ||
|
|
||
| private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1, GreenNode? child2, GreenNode? child3) | ||
| { | ||
| if (parent is null) | ||
| return false; | ||
|
|
||
| Debug.Assert(IsCacheable(parent)); | ||
|
|
||
| // If the parent kind matches, the slot count must match as well. Only if the parent kind does not match | ||
| // (so two different node kinds hashed to the same array location) could the slot counts differ. | ||
| Debug.Assert(parent.RawKind != kind || parent.SlotCount == 2); | ||
|
|
||
| return parent.RawKind == kind && | ||
| parent.Flags == flags && | ||
| parent.GetSlot(0) == child1 && | ||
| parent.GetSlot(1) == child2 && | ||
| parent.GetSlot(2) == child3; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
this code was moved. with no changes.