-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Break hangs on HashSet when a loop is formed on entries due to a concurrent operation #28225
Conversation
|
||
if (collisionCount >= _slots.Length) | ||
{ | ||
// The chain of entries forms a loop; which means a concurrent update has happened. |
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.
Nit: ";" => ","
|
||
if (collisionCount >= _slots.Length) | ||
{ | ||
// The chain of entries forms a loop; which means a concurrent update has happened. |
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.
Ditto (for all such cases)
if (collisionCount >= _slots.Length) | ||
{ | ||
// The chain of entries forms a loop; which means a concurrent update has happened. | ||
throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported); |
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.
We should probably use a ThrowHelper scheme instead of throw for these as some of these loops are particularly hot paths. There is not a ThrowHelper.cs in CoreFX common (probably should be). You can copy one of the several sprinkled around CoreFX sources.
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.
There is not a ThrowHelper.cs in CoreFX common (probably should be)
@Anipik Alpine build failed:
In a quick look, I think print("An unexpected error was encountered while installing dumpling.py: " + traceback.format_exc()) Could you please fix in dumplinghelper.py. |
{ | ||
internal static class ThrowHelper | ||
{ | ||
internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() => throw CreateInvalidOperationException_ConcurrentOperationsNotSupported(); |
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 can be just:
internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() => throw new InvalidOperationException(SR.InvalidOperation_ConcurrentOperationsNotSupported);
The complex throw helper pattern with intermediate CreateInvalidOperationException is a necessary only for cases where the code needs to be compatible with old JITs that do not perform the right optimizations. It is not the case for System.Collections.
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 see. Thought it was for all cases. Thanks, will update the PR.
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 drop in the bucket since the rest of the Hashset is not using the ThrowHelper. Using it in one place only helps very little.
These manual throw helpers are pain to deal with for Mono folks who are trying to reuse the code. cc @marek-safar
Unless this has a measurable performance impact for something that matters, we should avoid introducing these manual throw helpers.
If we care about the performance benefits, we should build the plugin for IL linker that autogenerates them everywhere.
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.
If we care about the performance benefits, we should build the plugin for IL linker that autogenerates them everywhere.
👍 I'd be very happy with that :)
{ | ||
internal static class ThrowHelper | ||
{ | ||
internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() => throw CreateInvalidOperationException_ConcurrentOperationsNotSupported(); |
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 drop in the bucket since the rest of the Hashset is not using the ThrowHelper. Using it in one place only helps very little.
These manual throw helpers are pain to deal with for Mono folks who are trying to reuse the code. cc @marek-safar
Unless this has a measurable performance impact for something that matters, we should avoid introducing these manual throw helpers.
If we care about the performance benefits, we should build the plugin for IL linker that autogenerates them everywhere.
So you would suggest updating HashSet to use ThrowHelper in all other places where is throwing, or measure if it is worth it, or not using ThrowHelper at all? |
I would suggest not using the manual ThrowHelper at all. |
@@ -265,6 +266,13 @@ public bool Contains(T item) | |||
{ | |||
return true; | |||
} | |||
|
|||
if (collisionCount >= _slots.Length) |
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.
Take _slots
to a local ref for its multiple uses?
Slot[] slots = _slots;
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 curious and to learn, what is the benefit of doing that?
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.
It uses the array in a register rather than a memory location; also applies extra optimisations because it cannot change between uses unlike the memory location version
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.
Makes sense. Thanks for explaining :)
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.
@benaadams is this something the JIT might be able to do itself in future? Always a shame to write code in an artificial way.
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.
Possibly, though bounds checks have already regressed to become even more conservative when the array comes from memory dotnet/coreclr#15756
@@ -293,6 +301,7 @@ public bool Remove(T item) | |||
int hashCode = InternalGetHashCode(item); | |||
int bucket = hashCode % _buckets.Length; | |||
int last = -1; | |||
int collisionCount = 0; | |||
for (int i = _buckets[bucket] - 1; i >= 0; last = i, i = _slots[i].next) |
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.
Take _slots
to a local ref for its multiple uses?
Slot[] slots = _slots;
Ok. Will update to throw from the class itself. Adding a private method that throws the exception would help instead of throwing it inside every instance? |
This file does not use the throw helpers at all. I would maintain the consistent style. Throw inplace without any helpers methods like vast majority of the code out there. |
@jkotas looks good now? |
@dotnet-bot test this please |
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.
Thanks
} | ||
else | ||
{ | ||
if (_lastIndex == _slots.Length) | ||
if (_lastIndex == slots.Length) | ||
{ | ||
IncreaseCapacity(); |
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.
Need to grab slots
again here as IncreaseCapacity()
will change it. So
IncreaseCapacity();
slots = _slots;
} | ||
else | ||
{ | ||
if (_lastIndex == _slots.Length) | ||
if (_lastIndex == slots.Length) | ||
{ | ||
IncreaseCapacity(); |
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.
Need to grab slots
again here as IncreaseCapacity()
will change it. So
IncreaseCapacity();
slots = _slots;
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.
Ah, you're right, thanks for the heads up.
test this please |
test Windows x64 Debug Build |
test Linux x64 Debug Build |
test Linux x64 Debug Build |
@mmitche seems like there is a bug in the regex job trigger. I triggered 2 specific jobs and it restarted 3 as you can see above. This happened for the last 2 times. |
test Linux x64 Debug Build please |
@danmosemsft the failures here doesn’t look related, good to merge? the Linux builds seem in a pretty flaky state. |
Not sure how to get the 1 failure
|
Please open an issue for the sockets failure you hit here. |
Fedora.26.Amd64.Open-x64-Debug
|
Thanks @benaadams |
…urrent operation (dotnet/corefx#28225) * Break hangs on HashSet when a loop is formed on entries due to a concurrent operation * PR Feedback, copy _slots to local ref * Get a copy of -slots after IncreaseCapacity changes it Commit migrated from dotnet/corefx@be48e96
Fixes: https://github.com/dotnet/corefx/issues/28209
cc: @danmosemsft @vancem @jkotas @stephentoub