Proposal: Generic Specialization #1315
Replies: 28 comments 8 replies
-
Sorry, not quite getting the proposal. Can you explain what this generates: public struct GenericType<TList>
: where TList : IList<int>
: specialize List<int>, int[] Basically, what would the C# compiler do with this? Thanks! |
Beta Was this translation helpful? Give feedback.
-
Is this a compiler feature? or compiler+runtime feature? i.e. what does:
result in? and is there a distinction in the type depending on whether the type was known at compile-time? How do dependency chains work? if 6 levels sit between |
Beta Was this translation helpful? Give feedback.
-
It would duplicate the struct creating public struct GenericType<TList> : where TList : IList<int>
public struct GenericType<List<int>>
public struct GenericType<int[]> The However, the public struct GenericType<List<int>>
public struct GenericType<int[]> As is non-shared code. The current way around this is to create a struct wrapper; and then call with that, but its a bit ugly new GenericType<ListIntWrapper>(new ListIntWrapper(list));
new GenericType<ArrayIntWrapper>(new ArrayIntWrapper(array)); struct ListIntWrapper : IList<int>
{
List<int> _list;
public ListIntWrapper(List<int> list) => _list = list;
public int this[int index] { get => _list[index]; set => _list[index] = value; }
public int Count => _list.Count;
public bool IsReadOnly => ((IList<int>)_list).IsReadOnly;
public void Add(int item) => _list.Add(item);
public void Clear() => _list.Clear();
public bool Contains(int item) => _list.Contains(item);
public void CopyTo(int[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
public List<int>.Enumerator GetEnumerator() => _list.GetEnumerator();
public int IndexOf(int item) => _list.IndexOf(item);
public void Insert(int index, int item) => _list.Insert(index, item);
public bool Remove(int item) => _list.Remove(item);
public void RemoveAt(int index) => _list.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
}
struct ArrayIntWrapper : IList<int>
{
int[] _array;
public ArrayIntWrapper(int[] array) => _array = array;
public int this[int index] { get => _array[index]; set => _array[index] = value; }
public int Count => _array.Length;
public bool IsReadOnly => ((IList<int>)_array).IsReadOnly;
public void Add(int item) => ((IList<int>)_array).Add(item);
public void Clear() => Array.Clear(_array, 0, _array.Length);
public bool Contains(int item) => _array.Contains(item);
public void CopyTo(int[] array, int arrayIndex) => _array.CopyTo(array, arrayIndex);
public int IndexOf(int item) => Array.IndexOf(_array, item);
public IEnumerator GetEnumerator() => _array.GetEnumerator();
public void Insert(int index, int item) => ((IList<int>)_array).Insert(index, item);
public bool Remove(int item) => ((IList<int>)_array).Remove(item);
public void RemoveAt(int index) => ((IList<int>)_array).RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => _array.GetEnumerator();
IEnumerator<int> IEnumerable<int>.GetEnumerator() => ((IList<int>)_array).GetEnumerator();
} |
Beta Was this translation helpful? Give feedback.
-
Likely you'd call the shared version; you've started paying cost with reflection, behaving same performance wise as before is probably valid? (Maybe the Jit would fix you up?) |
Beta Was this translation helpful? Give feedback.
-
I'm not sure what this is: public struct GenericType<TList> : where TList : IList<int>
public struct GenericType<List<int>>
public struct GenericType<int[]> That's not legal C# code. What does it mean to have a generic type who's type parameter is a type argument? Thanks! |
Beta Was this translation helpful? Give feedback.
-
This feels very similar to Shapes (#164) to me. Especially when you talk about the wrappers. It seems like you'd define a shape describing the capabilities you needed. Then you'd provide types that wrapped another type with that shape (preferably happening implicitly), and then your calls would always be optimized fully since shapes are done with structs, so the runtime always gives you a type specific implementation for each instantiation of hte shape. |
Beta Was this translation helpful? Give feedback.
-
That's why its the proposal: 😄 ProposalAllow specialist implementations of a Generic type to be pre-defined; including for class types public struct GenericType<T> : IEquatable<GenericType<T>>, IFormattable {}
public struct GenericType<byte> : IEquatable<GenericType<byte>>, IFormattable {}
public struct GenericType<string> : IEquatable<GenericType<string>>, IFormattable {} If a specific generic type is specified this will take precedence over the |
Beta Was this translation helpful? Give feedback.
-
@benaadams What if you have a |
Beta Was this translation helpful? Give feedback.
-
A parallel would be extensions where you can override a generic argument by using a more concrete one static class GenericTypeExtensions
{
public static void DoThing<TList>(this GenericType<TList> list) where TList : IList<int> {}
public static void DoThing(this GenericType<List<int>> list) { }
public static void DoThing(this GenericType<int[]> list) { }
} However you can't do with with instance methods; which is what the proposal is about |
Beta Was this translation helpful? Give feedback.
-
Yeah, shapes seems much better for this. With shapes you'd just do: public shape SList<T> { /*list members*/ }
public extension ListExt<T> of IList<T> : SList<T>;
public struct GenericType<TList> : where TList : SList<int>
{
// Your members
} Now, you'd just do: new GenericType<int[]>() and you'd get all the specialization you want. That's because it would translate into "new GenericType<ListExt<int[]>>()" and inside GenericType, |
Beta Was this translation helpful? Give feedback.
-
@benaadams But if you're working inside a generic type, the compiler can't know what overload to pick. This only works with templates unfortunately. If you have a specialized |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi as far as I can tell Shapes seems to fix every issue I raise... I want these magical beasts |
Beta Was this translation helpful? Give feedback.
-
That would juts be: public static int DoThing<TList>(TList list) where TList : SList<int> {
var sum = 0;
for (var i = 0; i < list.Length; i++) { sum += list[i]; }
return sum;
} You can then just call this as DoThing<int[], SyntheticCompilerGeneratedIntArray_SlistImpl>(someIntArray); And int DoThing<T, TImpl>(T list) where Tmpl : struct, SList<T> {
var impl = default(TImpl);
var sum = 0;
for (var i = 0; i < impl .get_Length(list); i++) { sum += impl .get_Item(list, i); }
return sum;
} |
Beta Was this translation helpful? Give feedback.
-
Basically, you can call these shape/extensions methods and as long as you show how your type abides by the shape, you effectively always get the specialized version. Stuff like int get_Length(int[] array) => array.Length; everything should inline. Basically it's like a double dispatch system, except where everything is known at codegen time, so all the actual bouncing around can be totally elided. |
Beta Was this translation helpful? Give feedback.
-
#preach |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi what does |
Beta Was this translation helpful? Give feedback.
-
The runtime is smart. This is a "constrained call" on a type-parameter value with some constraints. It knows that when you do a constrained call on a struct that it doesn't have to do virtual dispatch as structs can't be overridden. i.e. ignore shapes for a second. Say i just have: interface IList<T> { /* normal members */ }
struct StructList<T> : IList<T> { /* impl */ }
void Foo<T, TList>(TList list) where TList : struct, IList<T> {
// I can call IList members on 'list' because of the contraint.
}
Foo<int, StructList<int>>(someStructListOfInts); Here, because Foo is instantiated with value types, it gets a unique jit compilation. The jit sees the constrained calls to IList members in the body of Foo. But, because it knows the final result type and sees that it is a struct, it can avoid virtual dispatch entirely and just call the actual concrete, non-virtual method that implements the IList member. -- In essence, this is what shapes compile down to. From the linked to proposal:
The runtime already does this trick. And, if shapes were to come to pass, i bet they could even take "near-zero cost" down to zero, with the only overhead being the jit compilation step, but the final runtime code being exactly what you would want it to be. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi could you please create an FAQ section describing what cannot be achieved by using shapes, source generators and extension-everything? |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi if I understand correctly don't think it will work; as it needs to be a concrete rather than generic struct. So this will cause the optimization struct ListIntWrapper : IList<int>
{
List<int> _list;
} but this won't struct IListWrapper<TList> where TList : IList<int>
{
TList _list;
} As the generic code portion will still use shared code for the object path; I believe |
Beta Was this translation helpful? Give feedback.
-
In Shapes, it's always a concrete struct at the end of the day i believe. (but i could be wrong). Best to ask on the shape discussion and seek clarification. |
Beta Was this translation helpful? Give feedback.
-
@benaadams It would be helpful to have a concrete example of what your'e trying to do. i.e. don't just write "i want public struct |
Beta Was this translation helpful? Give feedback.
-
I think type classes should cover this case (warning: the example is severely similar to Rust's) // TSelf is an implicit type parameter available to all traits
trait Eq</* TSelf, */ Rhs = TSelf> {
static bool operator ==(TSelf self, Rhs other);
static bool operator !=(TSelf self, Rhs other) => !(self == other);
}
// specialized implementations
implement Eq for C<byte> { // TSelf = C<byte>, Rhs = TSelf = C<byte>
static bool operator ==(C<byte> self, C<byte> other) { ... }
}
implement Eq for C<string> { // TSelf = C<string>, Rhs = TSelf = C<string>
static bool operator ==(C<string> self, C<string> other) { ... }
}
// fallback/generic implementation
implement<T> Eq for C<T> where T : Eq { // TSelf = C<T>, Rhs = TSelf = C<T>
static bool operator ==(C<T> self, C<T> other) { ... }
} After that, overload resolution determines the precise member to be invoked. |
Beta Was this translation helpful? Give feedback.
-
@dsaf Shapes/Extensions and Source Generators would not be appropriate for Higher Kinded Polymorphism (#339). |
Beta Was this translation helpful? Give feedback.
-
I wanted to have generic specialization (full and partial!) ever since I came back to .NET from C++ around 8 years ago, but there was no repo to report that problem anywhere back then :) Two suggestions for consideration: Partial specializationMy first suggestion is to explicitly define whether partial specialization (in C++ sense) is supposed to be possible or not, because the examples don't show it. I believe that partial specialization is great for further improving development of high performance code on value types. With partial specialization you can do things like:
Generic type specialization scope inside typeof(T) comparison blocksAnother suggestion is related to the typeof(T) == typeof(value_type) comparison.
My suggestion is to make the compiler aware of such comparisons in that they create compile-time type materialization. JIT alread recognizes such constness, and removes the statement away when its true, but you still need to box and unbox to get it to compile. Perhaps implementing such awareness is much easier in the compiler than rolling out full generics specialization and/or Shapes. I agree it is less readable than specialization, but in contrast to specialization it's not requiring new syntax. |
Beta Was this translation helpful? Give feedback.
-
It isn't. C# generics are not at all like C++ templates. Think of it like this: The IL for a generic type is not usable immediately, it's merely a blueprint. When constructing, the runtime creates a copy of that blueprint to make the actual type instance from. This copy then remains cached for re-use (only a single copy when all type arguments are reference types, and a separate copy for each permutation of value types used). C++ compiles down to platform-specific code (most of the time), meaning their compilers (and probably even language spec) can make decisions based on knowledge from the entire program, which I think is what makes its specialization possible in the first place. (On the other hand, C++ has no way to constrain type parameters, last I heard.) If anything, support for specialization à la C++ should first be surfaced in the CLR, since that's the only thing that even knows the whole program. After that, it can be discussed how it'll get supported in the language.
Having to write
I doubt that, because there are still other .NET runtimes and the compiler has to emit IL that should work on any of those. So let's say Microsoft's CLR can elide the boxing, that still means every other runtime can't. If you then had the ability to write only |
Beta Was this translation helpful? Give feedback.
-
Of course they aren't at this point of time, but this this original issue is a proposal to extend their expressiveness. I'm merely suggesting additional points for consideration. I know the C++ templates vs C# generics bullet points, however there are valid use cases for more powerful parametrized types, especially when performance is a concern.
My second proposal should not require knowledge of the entire program, it's similar to how constraints work in restricting the types for a block (a method or a class). Moreover, you can nest constraints and combine them in some ways. The typeof specialization is conceptually similar to adding a generic constraint on a block, albeit allowing for concrete types. It would not break existing code, but add a way to simplify it + get performance boost without relying on the JIT. Old, expensive, box + unbox would still be valid. If you are aware of a related JIT proposal change for optimizing away the box + unbox, please add a link to it. If this is the only way to get it soon, I'm happy to live with ugly syntax if the performance is right. |
Beta Was this translation helpful? Give feedback.
-
Related research project: Claudio Russo, Matt Windsor: Type Classes for the Masses https://github.com/MattWindsor91/roslyn/blob/master/concepts/docs/csconcepts.md#performance |
Beta Was this translation helpful? Give feedback.
-
I'd want this so much. This relates in an interesting way to this discussion: For example, if I'm writing DSP code and I want an FFT for floats and an FFT for doubles, I might want to do an API like |
Beta Was this translation helpful? Give feedback.
-
From: dotnet/roslyn#15822
Related: "Adding a 'static if' construct to support generic specialization" dotnet/roslyn#8871
Currently generics create a duplicate code path for structs and share one for classes
This can be used to specialize the generic type for individual structs using
typeof
which are then removed at jit time to make branchless paths;System.Numeric.Vectors
is probably the most extreme example of this.However even this advantage for structs can lead to oddities (as well as very large functions); such as casting values through object to convert
T
to valuetypeProposal
Allow specialist implementations of a Generic type to be pre-defined; including for class types
If a specific generic type is specified this will take precedence over the
T
version; otherwise will work as now.Bonus Round
Allow templating "specialization" where the overload is automatically generated
Where the specializations will automatically be generated for those class types to take advantage of inlining etc
Questions
Beta Was this translation helpful? Give feedback.
All reactions