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

Reduce code size impact of ArrayPool<T> #97058

Merged
merged 2 commits into from
Jan 17, 2024

Conversation

stephentoub
Copy link
Member

Every SharedArrayPool<T> brings with it several types, including several array types and all of their interface implementations. This makes the internals of SharedArrayPool a bit less generic to try to reduce that impact. The changes are effectively:

  • Move the nested Partition, Partitions, and ThreadLocalArray types out from being nested types to being peers
  • Rename them, since they're no longer inheriting the parent's name
  • Make them non-generic in terms of Array rather than generic in terms of T[]. These types never index into the array, so other than accessing Length for logging purposes, it could even have used object.
  • Use Unsafe.As in the two places arrays are dequeued and need to be T[] rather than Array.
  • Pass in the sizeof(T) rather than using it in the implementation. The size is used only when trimming to determine how much to trim.

From measuring locally, this looks to get back ~120K of the ~200K that came from changing Enumerable.ToArray to use the ArrayPool. I have a separate change that targets ToArray directly, but it's not resulting in the trimming I was expecting and am following up offline.

Every `SharedArrayPool<T>` brings with it several types, including several array types and all of their interface implementations. This makes the internals of SharedArrayPool a bit less generic to try to reduce that impact.  The changes are effectively:
- Move the nested Partition, Partitions, and ThreadLocalArray types out from being nested types to being peers
- Rename them, since they're no longer inheriting the parent's name
- Make them non-generic in terms of Array rather than generic in terms of T[]. These types never index into the array, so other than accessing Length for logging purposes, it could even have used object.
- Use Unsafe.As in the two places arrays are dequeued and need to be T[] rather than Array.
- Pass in the sizeof(T) rather than using it in the implementation. The size is used only when trimming to determine how much to trim.
@ghost
Copy link

ghost commented Jan 16, 2024

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

Issue Details

Every SharedArrayPool<T> brings with it several types, including several array types and all of their interface implementations. This makes the internals of SharedArrayPool a bit less generic to try to reduce that impact. The changes are effectively:

  • Move the nested Partition, Partitions, and ThreadLocalArray types out from being nested types to being peers
  • Rename them, since they're no longer inheriting the parent's name
  • Make them non-generic in terms of Array rather than generic in terms of T[]. These types never index into the array, so other than accessing Length for logging purposes, it could even have used object.
  • Use Unsafe.As in the two places arrays are dequeued and need to be T[] rather than Array.
  • Pass in the sizeof(T) rather than using it in the implementation. The size is used only when trimming to determine how much to trim.

From measuring locally, this looks to get back ~120K of the ~200K that came from changing Enumerable.ToArray to use the ArrayPool. I have a separate change that targets ToArray directly, but it's not resulting in the trimming I was expecting and am following up offline.

Author: stephentoub
Assignees: -
Labels:

area-System.Buffers

Milestone: -

@eerhardt
Copy link
Member

I assume there is no loss in throughput with this change? (Since the original change (#96570) listed perf benchmark results.)

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@stephentoub
Copy link
Member Author

I assume there is no loss in throughput with this change? (Since the original change (#96570) listed perf benchmark results.)

There was actually a small regression on an ArrayPool microbenchmark. This:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Buffers;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[DisassemblyDiagnoser]
public class Tests
{
    private List<string[]> _arrays = new();

    [Benchmark]
    public void RentReturn()
    {
        for (int i = 0; i < 10; i++)
        {
            _arrays.Add(ArrayPool<string>.Shared.Rent(100));
        }
        foreach (string[] array in _arrays)
        {
            ArrayPool<string>.Shared.Return(array);
        }
        _arrays.Clear();
    }
}

got ~5-10% slower depending on the run locally. I fixed it with c7b9c24, and that same benchmark is now ~5% faster than main locally.

I'm not in love with having to use Unsafe for that, but I'm not aware of another good way to avoid the covariance check. It was previously elided by the JIT because the type in question was sealed, but Array isn't.

@stephentoub
Copy link
Member Author

All failures are known.

@stephentoub stephentoub merged commit fe3a2b9 into dotnet:main Jan 17, 2024
175 of 178 checks passed
@stephentoub stephentoub deleted the partitionsize branch January 17, 2024 15:28
tmds pushed a commit to tmds/runtime that referenced this pull request Jan 23, 2024
* Reduce code size impact of `ArrayPool<T>`

Every `SharedArrayPool<T>` brings with it several types, including several array types and all of their interface implementations. This makes the internals of SharedArrayPool a bit less generic to try to reduce that impact.  The changes are effectively:
- Move the nested Partition, Partitions, and ThreadLocalArray types out from being nested types to being peers
- Rename them, since they're no longer inheriting the parent's name
- Make them non-generic in terms of Array rather than generic in terms of T[]. These types never index into the array, so other than accessing Length for logging purposes, it could even have used object.
- Use Unsafe.As in the two places arrays are dequeued and need to be T[] rather than Array.
- Pass in the sizeof(T) rather than using it in the implementation. The size is used only when trimming to determine how much to trim.

* Fix throughput regression from covariance check
@github-actions github-actions bot locked and limited conversation to collaborators Feb 17, 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.

3 participants