Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -526,20 +526,22 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
uint hashCode = (uint)((typeof(TKey).IsValueType && comparer == null) ? key.GetHashCode() : comparer!.GetHashCode(key));

uint collisionCount = 0;
uint entriesLength = (uint)entries.Length;
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)
Copy link
Member

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];
Copy link
Member

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?

Copy link
Contributor Author

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

if (e.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(e.key, key))
{
if (behavior == InsertionBehavior.OverwriteExisting)
{
entries[i].value = value;
e.value = value;
return true;
}

Expand All @@ -551,10 +553,10 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
return false;
}

i = entries[i].next;
i = e.next;

collisionCount++;
if (collisionCount > (uint)entries.Length)
if (collisionCount > entriesLength)
{
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
Expand All @@ -565,13 +567,14 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
else
{
Debug.Assert(comparer is not null);
while ((uint)i < (uint)entries.Length)
while ((uint)i < entriesLength)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
ref Entry e = ref entries[i];
if (e.hashCode == hashCode && comparer.Equals(e.key, key))
{
if (behavior == InsertionBehavior.OverwriteExisting)
{
entries[i].value = value;
e.value = value;
return true;
}

Expand All @@ -583,10 +586,10 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior)
return false;
}

i = entries[i].next;
i = e.next;

collisionCount++;
if (collisionCount > (uint)entries.Length)
if (collisionCount > entriesLength)
{
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
Expand Down Expand Up @@ -1091,26 +1094,28 @@ internal static class CollectionsMarshalHelper
uint hashCode = (uint)((typeof(TKey).IsValueType && comparer == null) ? key.GetHashCode() : comparer!.GetHashCode(key));

uint collisionCount = 0;
uint entriesLength = (uint)entries.Length;
ref int bucket = ref dictionary.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)
{
if (entries[i].hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entries[i].key, key))
ref Entry e = ref entries[i];
if (e.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(e.key, key))
{
exists = true;

return ref entries[i].value!;
return ref e.value!;
}

i = entries[i].next;
i = e.next;

collisionCount++;
if (collisionCount > (uint)entries.Length)
if (collisionCount > entriesLength)
{
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
Expand All @@ -1121,19 +1126,20 @@ internal static class CollectionsMarshalHelper
else
{
Debug.Assert(comparer is not null);
while ((uint)i < (uint)entries.Length)
while ((uint)i < entriesLength)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
ref Entry e = ref entries[i];
if (e.hashCode == hashCode && comparer.Equals(e.key, key))
{
exists = true;

return ref entries[i].value!;
return ref e.value!;
}

i = entries[i].next;
i = e.next;

collisionCount++;
if (collisionCount > (uint)entries.Length)
if (collisionCount > entriesLength)
{
// The chain of entries forms a loop; which means a concurrent update has happened.
// Break out of the loop and throw, rather than looping forever.
Expand Down
Loading