dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.25k stars 4.73k forks source link

[API Proposal]: Enumerable.TryGetSpan(IEnumerable<T> enumerable, out ReadOnlySpan<T> span); #58821

Open pranavkm opened 3 years ago

pranavkm commented 3 years ago

Background and motivation

A number of APIs expose or consume an IEnumerable<T> which are often backed by a List<T> or an array. A motivated user might want to avoid the GetEnumerator allocation in these cases. It would be nice if Enumerable provided an API similar to TryGetNonEnumeratedCount to get at the backing span for allocation free enumeration

API Proposal

namespace System.Linq
{
     public static class Enumerable
     {
          public static bool TryGetSpan(this IEnumerable<T> enumerable, out ReadOnlySpan<T> span);
     }
}

API Usage

// Fancy the value
IEnumerable<string> eumerable = new[] { "Hello", "World" };
if (enumerable.TryGetSpan(enumerable, out ReadOnlySpan<string> span))
{
   foreach (var item in span)
   {
      // 
   }
}
else
{
   throw new OutOfMemoryException("Allocation free or die trying.");
}

Risks

n/a

ghost commented 3 years ago

Tagging subscribers to this area: @eiriktsarpalis See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation A number of APIs expose or consume an `IEnumerable` which are often backed by a `List` or an array. A motivated user might want to avoid the `GetEnumerator` allocation in these cases. It would be nice if `Enumerable` provided an API similar to `TryGetNonEnumeratedCount` to get at the backing `span` for allocation free enumeration ### API Proposal ```C# namespace System.Linq { public static class Enumerable { public static bool TryGetSpan(this IEnumerable enumerable, out ReadOnlySpan span); } } ``` ### API Usage ```C# // Fancy the value IEnumerable eumerable = new[] { "Hello", "World" }; if (enumerable.TryGetSpan(enumerable, out ReadOnlySpan span) { foreach (var item in span) { // } } else { throw new OutOfMemoryException("Allocation free or die trying."); } ``` ### Risks n/a
Author: pranavkm
Assignees: -
Labels: `api-suggestion`, `area-System.Linq`, `untriaged`
Milestone: -
GrabYourPitchforks commented 3 years ago

Given that this could allow violating some internal invariants of the underlying collection, this should not be an extension method. If added, it should probably go on CollectionsMarshal. See also https://github.com/dotnet/runtime/issues/27061.

eiriktsarpalis commented 3 years ago

Currently CollectionMarshal is restricted to accessing storage internals of concrete data types. I'm not sure how I feel about extending this to an interface: we'd basically be supporting a handful of known implementations and returning false for everything else.

ghost commented 3 years ago

Tagging subscribers to this area: @eiriktsarpalis See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation A number of APIs expose or consume an `IEnumerable` which are often backed by a `List` or an array. A motivated user might want to avoid the `GetEnumerator` allocation in these cases. It would be nice if `Enumerable` provided an API similar to `TryGetNonEnumeratedCount` to get at the backing `span` for allocation free enumeration ### API Proposal ```C# namespace System.Linq { public static class Enumerable { public static bool TryGetSpan(this IEnumerable enumerable, out ReadOnlySpan span); } } ``` ### API Usage ```C# // Fancy the value IEnumerable eumerable = new[] { "Hello", "World" }; if (enumerable.TryGetSpan(enumerable, out ReadOnlySpan span)) { foreach (var item in span) { // } } else { throw new OutOfMemoryException("Allocation free or die trying."); } ``` ### Risks n/a
Author: pranavkm
Assignees: -
Labels: `api-needs-work`, `api-suggestion`, `area-System.Collections`
Milestone: -
zlatanov commented 3 years ago

What about having this directly in the interface? Something like:

public interface IEnumerable<out T>
{
    public bool TryGetSpan(out ReadOnlySpan<T> span)
    {
        span = default;
        return false;
    }
}

Then any implementation of IEnumerable<T> could also have an implementation for this if it wishes and can.

eiriktsarpalis commented 3 years ago

Adding DIMs to existing interfaces (particularly very popular ones) can introduce both source and runtime breaking changes due to the potential of diamond ambiguities arising in type hierarchies. See this repo for an example.

zlatanov commented 3 years ago

Adding DIMs to existing interfaces (particularly very popular ones) can introduce both source and runtime breaking changes due to the potential of diamond ambiguities arising in type hierarchies. See this repo for an example.

This makes me sad. I thought the primary use case for DIMs was to allow non-breaking evolution for interfaces. In my opinion, adding such functionality in utility classes such as CollectionsMarshal is not good for the community and library authors, as such functionality will be restricted to the BCL only.

davidfowl commented 3 years ago

@eiriktsarpalis why future? Is that .NET 7?

eiriktsarpalis commented 3 years ago

We could consider it for inclusion in .NET 7, but as it stands the proposed design has a few issues in my view:

  1. It accepts the all-encompasing IEnumerable<T> when in reality it is only supporting a handful of know concrete collection types (likely only very popular ones living in System.Private.CoreLib). This could violate user expectations (who may for example expect CopyTo-like semantics for things that implement ICollection<T>).
  2. We would likely be constrained on the number of concrete types that we support (out of performance considerations when multiple runtime type tests are being added to the method).
  3. Such a method would likely differ in no way from one that was rolled by a user employing runtime type tests and using existing CollectionsMarshal methods that act on concrete collections. If anything, it might be better since it lets them focus specifically in the collection types they are interested in:
    bool MyTryGetSpan(this IEnumerable<T> source, out ReadOnlySpan<T> ros)
    {
    if (source is T[] arr)
    {
        ros = arr;
        return true;
    }
    if (source is List<T> list)
    {
        ros = CollectionsMarshal.GetSpan(list);
        return true;
    }
    if (source is MyCollection<T> myColl)
    {
        // some other collection type defined in a user application
        ros = myColl._buffer;
        return true;
    }
    }