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

Avoid boxing in System.ObjectModel.KeydCollection during startup #104504

Closed
wants to merge 1 commit into from

Conversation

jkotas
Copy link
Member

@jkotas jkotas commented Jul 6, 2024

ArgumentNullException.ThrowIfNull can incur boxing when applied to argument of generic type. Tier-1 JIT optimizations are able to optimize this boxing in steady state, but Tier-0 JIT optimization are not. It can result into excessive allocations during startup. Switch KeyedCollection to use ThrowHelper that is pattern used by number of other collections.

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jul 6, 2024
@jkotas
Copy link
Member Author

jkotas commented Jul 6, 2024

This was noted in #103361 (comment) .

@jkotas jkotas changed the title Avoid boxing System.ObjectModel during startup Avoid boxing in System.ObjectModel.KeydCollection during startup Jul 6, 2024
ArgumentNullException.ThrowIfNull can incur boxing when applied to argument
of generic type. Tier-1 JIT optimizations are able to optimize this boxing
in steady state, but Tier-0 JIT optimization are not. It can result into excessive
allocations during startup. Switch KeyedCollection to use ThrowHelper
that is pattern used by number of other collections.
@jkotas
Copy link
Member Author

jkotas commented Jul 6, 2024

Repro:

using System.Collections.ObjectModel;

var c = new MyCollection();

var start = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1000; i++)
{
    c.TryGetValue(1, out _);
}
var end = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine(end - start);

class MyCollection : KeyedCollection<uint, uint>
{
    protected override uint GetKeyForItem(uint item) => item;
}

24000 before the change, 0 with the change.

@jkotas jkotas added area-System.Collections and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jul 6, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

@stephentoub
Copy link
Member

stephentoub commented Jul 6, 2024

Issues on this (not just for KeyedCollection, but multiple places ThrowIfNull is used with a generic) have been opened multiple times in the past, and we've previously defended the non-generic ThrowIfNull for such use, citing the boxing removal by the JIT. If we're going to start replacing these, I think we should instead reconsider adding such a generic overload, or augment the JIT to do the boxing removal even in tier 0.

@jkotas
Copy link
Member Author

jkotas commented Jul 6, 2024

Issues on this (not just for KeyedCollection, but multiple places ThrowIfNull is used with a generic) have been opened multiple times in the past

I was not aware that we had issues opened on this in the past. For reference: #82227 and #100406

On the other hand, we took tweaks to avoid Tier-0 specific boxing in the past.

we should instead reconsider adding such a generic overload

Is there a way to make this generic overload bind to generic types only? If we just add a generic overload, it would be preferred over the object overload, and we would end up with a ton of generic instantiations. The startup cost of these generic instantiations during startup of a typical app would be likely a lot more than the cost of the occasional boxing caused by this during startup of a typical app.

augment the JIT to do the boxing removal even in tier 0.

@dotnet/jit-contrib What would it take to augment Tier-0 to avoid boxing in this case?

@stephentoub
Copy link
Member

stephentoub commented Jul 6, 2024

Is there a way to make this generic overload bind to generic types only?

Not to my knowledge. @333fred, any way to use overload priorities to achieve this?

@AndyAyersMS
Copy link
Member

@dotnet/jit-contrib What would it take to augment Tier-0 to avoid boxing in this case?

We'd have to mark the method as intrinsic and add new logic to impBoxPatternMatch.

@333fred
Copy link
Member

333fred commented Jul 6, 2024

Is there a way to make this generic overload bind to generic types only?

Not to my knowledge. @333fred, any way to use overload priorities to achieve this?

To be clear, you'd want public static void ThrowIfNull<T>(T? argument, string? paramName = default), but for that to only be prioritized when T is itself a type parameter? No, that isn't possible to do with priorities as designed. Priority is applied unconditionally.

@jkotas
Copy link
Member Author

jkotas commented Jul 6, 2024

We'd have to mark the method as intrinsic and add new logic to impBoxPatternMatch.

That sounds better to me than applying workarounds like this in generic collections and other types. Opened #104512

@jkotas jkotas closed this Jul 6, 2024
@jkotas jkotas deleted the boxing branch July 6, 2024 19:14
@github-actions github-actions bot locked and limited conversation to collaborators Aug 6, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants