-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Optimize Dictionary<>.TryInsert()
by using ref var
#108119
base: main
Are you sure you want to change the base?
Conversation
Tagging subscribers to this area: @dotnet/area-system-collections |
I don't think this should be done. We were intently calling |
The JIT is not always smart.
to
Comparing with zero would be easier |
JIT already does this if it can prove for safety. The code you are touching includes some tricks for JIT. We should inspect the assembly code to ensure everything is working properly. |
Looked at the asm with
translates into
and the current code
translates into
So the JIT has proven that |
ref int bucket = ref GetBucket(hashCode); | ||
int i = bucket - 1; // Value in _buckets is 1-based | ||
|
||
if (typeof(TKey).IsValueType && // comparer can only be null for value types; enable JIT to eliminate entire if block for ref types | ||
comparer == null) | ||
{ | ||
// ValueType: Devirtualize with EqualityComparer<TKey>.Default intrinsic | ||
while ((uint)i < (uint)entries.Length) | ||
while ((uint)i < entriesLength) |
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.
Note: (uint)i <= (uint)array.Length
is already a trick for the JIT, when it can't prove i
for non-negative. Extracting array length into a variable is an anti-pattern and will only increase the chance for JIT to mess things up.
{ | ||
if (entries[i].hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entries[i].key, key)) | ||
ref Entry e = ref entries[i]; |
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.
I don't think this really matters. Array accessing is a trivial expression and JIT is capable to do the CSE. Is there any assembly difference for this?
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.
Array accessing is a trivial expression and JIT is capable to do the CSE.
The Dictionary
uses ref Entry
already in other place:
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L889
I'm referring to the disassembly for current change. Based on my knowledge I'd expect the change to result in zero difference.
This is already an optimization employed by JIT: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/#loops Also note that such micro tweaks are mostly handled the JIT folks. They know the current behavior of the JIT to optimize the code, and the architectural capabilities to optimize them in the near future. We do want to write code in more intuitive ways and only do such manual tweaks and tricks for critical cases. |
Well
to
to
The code looks equivally efficient. |
Have you tried running the full benchmark suite as defined in https://github.com/dotnet/performance/tree/main/src/benchmarks/micro/libraries/System.Collections? |
Benchmarking shows some improvement:
Windows, x64, .NET9
The
ref var
is calledEntry e
to avoid collision withEntry entry
in the outer scope.Also:
entries.Length
into variable to avoid extracting it two times on every iteration.CollectionsMarshalHelper.GetValueRefOrAddDefault()
is modified for consistency