-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Expose an attribute to allow sizing a fixed sized buffer based on type and element count. #12320
Comments
CC. @jkotas, @jaredpar Does this sound like something that would be feasible? |
Such an attribute might end up looking like: [AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class FixedSizedBufferAttribute : Attribute
{
public FixedSizedBufferAttribute(Type type, int count)
{
Type = type;
Count = count;
}
public Type Type { get; }
public int Count { get; }
} |
This wouldn't quite work for pointer types, since you can't have |
For anyone reading ... please don't do this in practice. It violates our rules around lifetimes. Assuming @tannergooding just meant this as an example of how to implement this. Not as guidance for how to AV your application 😄 This seems sensible from the language side. But it does mean that we'd end up tying this feature to |
Right, Ideally this would all be handled by the compiler safely and behind the scenes (similar to how indexing a single element in a regular fixed sized buffer no longer requires pinning). |
This would not work for generics. Attribute arguments cannot use type parameters.
FWIW, this is artificial C# limitation. You can have |
Is the behavior here to just return a |
That's a good point. I wonder if there is a good/clever way to workaround this limitation. Not supporting generics would probably still cover most interesting interop scenarios, but would make writing certain types of managed performance optimizations a bit troublesome.
@jaredpar, is this something that I should log a language request for? |
See https://github.com/dotnet/coreclr/issues/23408#issuecomment-475690498 |
A while ago we had a proposal for "Inlined Indexable Data", aka "struct arrays". I think you are really asking for struct arrays :-). With that you could do the following: public struct DXGI_RGB
{
public float Red;
public float Green;
public float Blue;
}
public struct DXGI_GAMMA_CONTROL
{
public DXGI_RGB Scale;
public DXGI_RGB Offset;
// IID variable. Can also be a class field, a parameter, a local, a threadstatic or whatever.
// DXGI_RGB[1025] is just a sugared struct that statically knows its size and can be indexed.
public DXGI_RGB[1025] data;
}
DXGI_GAMMA_CONTROL ctrl = default;
Span<DXGI_RGB> span = ctrl.data[..]; //span escape is tied to ref-escape of ctrl, so this is safe.
ManagedHelper(span);
fixed (void* pData = &ctrl.data)
{
NativeHelper(pData, ctrl.data.Length);
}
. . . I think struct arrays could be very useful, in particular for vector/matrix stuff like in graphics and ML. On the language side the feature is somewhat similar to tuples. There are N predefined structs and for N+ we nest. Perhaps a bit simpler than tuples, since there is only one TElement for the whole thing. There is much less type instantiation involved since nesting is tree-like not lists-like. There could be some support needed on the runtime side -
|
Yes, struct (or "value") arrays are basically a natural extension of the fixed-sized buffer proposal. I remember you and I had talked about them a couple of times in the past. I think one of the "key" differentiators is that I do think this distinction becomes less important if the marshaller is able to deal with generic types (CoreCLR doesn't today, but Mono does) and if it can treat them as blittable (assuming the equivalent non-generic struct would be blittable), but until that happens, it is actually a fairly important distinction to have. |
There is an important note relating to fixed-buffers and interop: Up until a few months ago, very little effort was put into enabling good interop scenarios for fixed-buffers. As a result (for compat reasons), we don't actually correctly support non-blittable fixed buffers, and likely never will for compat reasons (see dotnet/coreclr#20194, dotnet/coreclr#20375, dotnet/coreclr#20558, dotnet/coreclr#20575). |
@jkoritzinsky, wouldn't this new attribute allow it without breaking back-compat? That is, given that it requires explicit VM support and explicitly lists both the type and element count, it should always be marshallable correctly? |
(unlike the existing attributes, which really relies on heuristics) |
If Roslyn were to continue marking The problem is that some people may have been incorrectly using If Roslyn were to only mark non-primitive fixed buffers with the new attribute, we could more easily detect it and accurately account for it. However, it would be more difficult to correctly detect a char fixed buffer and fall back to the "legacy" behavior with the new system. |
@jkotas can chime in on the back-compat risk of non-blittable |
In terms of compat. I don't see any change to how we generate existing |
If unsafe is removed then we're in new territory I think (there are no C# programs with "non-unsafe" fixed buffers). In that case, we don't need to be bound to back-compat IMO and we can have full correctness. |
Simple way is "InlineArrayAttribute" on field that repeats the given field N times in the layout, e.g.:
With this attribute A more complex and ambitious option would be to allow const generic arguments: dotnet/csharplang#749 .
@jaredpar I am not sure what the question is. Here are a few related comments from the Unmanaged constructed types discussion: dotnet/csharplang#1744 (comment) dotnet/csharplang#1744 (comment)
Non-blittable structs are a pit of failure in interop. I do not think we would want to do anything for non-blittable types here (maybe just explicitly throw for them in the runtime to make it clear that they are not expected to work).
None of this runtime support would work down level, and our stated strategy is pick the right designs for new features and not limit them by what works downlevel. |
Just wondering - could Yeah. const generic arguments would be a much more general solution to these problems. :-) That would be a massive change though. |
Yes |
I immediately like the idea. |
As do I 👍 |
Up until this point I hadn't really thought of |
|
The confusion typically arises because for the purpose of computations (as opposed to storage) all pointer values are treated as They are real types though from the metadata and reflection point of view. There are some limitations - cannot box, cannot instantiate a generic type, etc.. , but those are just special cases, not because these are not real types. |
I'm not actually convinced that constant integer arguments would be a massive change at the runtime level. For instance, you could do something like this... interface INumber<T> { T GetValue(); }
struct _0 : INumber<uint> { uint GetValue() <= 0; }
struct _0<T> where T:INumber<uint> : INumber<uint> { uint GetValue() { T t = default(T); t.GetValue() * 10 + 0; }
struct _1 : INumber<uint> { uint GetValue() <= 1; }
struct _1<T> where T:INumber<uint> : INumber<uint> { uint GetValue() { T t = default(T); t.GetValue() * 10 + 1; }
struct _2 : INumber<uint> { uint GetValue() <= 3; }
struct _2<T> where T:INumber<uint> : INumber<uint> { uint GetValue() { T t = default(T); t.GetValue() * 10 + 2; }
// And so on, for all the other decimal values
// This struct has special implementation in the type loader, in that it requires N to be one of the types defined above, and that it makes itself bit enough.
// Also its interop treatment would be special. Something like, if T is blittable, then InlineArray<T, Anything> is blittable, otherwise, its not marshalable, or somethign to that effect.
struct InlineArray<T, N> where N:INumber<uint>
{
private T _t;
// Access via api something like this, appropriately annotated so that the C# compiler can do something safe.
public Span<T> AsSpan() { N num = default(N); MemberyHelpers.CreateSpan(ref _t, num.GetValue()); }
}
// We could then encode an array of size 21 by something like this
struct WithLargeInlineArray
{
InlineArray<int, _1<_2>> _field;
}
// I don't think this is a particularly pretty encoding, but it feels hideable by C# should we want it. This idea wouldn't allow arrays of pointers, and array's of ref structs would continue to be impossible, but it would generally work for a lot of other stuff. I actually think this would be simpler in cost to implement in the runtime as we could put all the special casing into the load of the InlineArray type instead of making the InlineArrayAttribute have to work in all the places we do field layout, field handling, interop, etc. Of course, its a much bigger conceptual idea than just an inline array, so that may be a good reason not to do it. |
Seems this works against 2.9.0 and current Not sure what lead me to believe it wasn't working. Possibly something misconfigured when I was testing things out. |
Ah, I think I realize what was wrong. while you can have |
FWIW here is the rough implementation of ValueArray in the prototype. For the simplicity in the prototype the branching factor is 4. namespace System
{
public interface IValueArray<T>
{
int Length { get; }
}
#pragma warning disable 169 //not writing to fields, ever.
public struct ValueArray1<T> : IValueArray<T>
{
// compiler uses this when read/write LValue is needed
public static ref T ItemRef(ref ValueArray1<T> array, int index)
=> ref ValueArrayHelpers.ItemRefImpl<T, ValueArray1<T>>(ref array, index);
// compiler uses this when readable LValue is needed
public static ref readonly T ItemRefReadonly(in ValueArray1<T> array, int index)
=> ref ValueArrayHelpers.ItemRefReadonlyImpl<T, ValueArray1<T>>(in array, index);
public int Length => 1;
// data
private T item0;
public T this[int i]
{
get
{
return ItemRef(ref this, i);
}
set
{
ItemRef(ref this, i) = value;
}
}
}
public struct ValueArray2<T> : IValueArray<T>
{
// compiler uses this when read/write LValue is needed
public static ref T ItemRef(ref ValueArray2<T> array, int index)
=> ref ValueArrayHelpers.ItemRefImpl<T, ValueArray2<T>>(ref array, index);
// compiler uses this when readable LValue is needed
public static ref readonly T ItemRefReadonly(in ValueArray2<T> array, int index)
=> ref ValueArrayHelpers.ItemRefReadonlyImpl<T, ValueArray2<T>>(in array, index);
public int Length => 2;
// data
private T item0;
private T item1;
public T this[int i]
{
get
{
return ItemRef(ref this, i);
}
set
{
ItemRef(ref this, i) = value;
}
}
}
public struct ValueArray3<T> : IValueArray<T>
{
// compiler uses this when read/write LValue is needed
public static ref T ItemRef(ref ValueArray3<T> array, int index)
=> ref ValueArrayHelpers.ItemRefImpl<T, ValueArray3<T>>(ref array, index);
// compiler uses this when readable LValue is needed
public static ref readonly T ItemRefReadonly(in ValueArray3<T> array, int index)
=> ref ValueArrayHelpers.ItemRefReadonlyImpl<T, ValueArray3<T>>(in array, index);
public int Length => 3;
// data
private T item0;
private T item1;
private T item2;
public T this[int i]
{
get
{
return ItemRef(ref this, i);
}
set
{
ItemRef(ref this, i) = value;
}
}
}
public struct ValueArrayN<T, T1, T2, T3, T4> : IValueArray<T>
where T1 : IValueArray<T>
where T2 : IValueArray<T>
where T3 : IValueArray<T>
where T4 : IValueArray<T>
{
// compiler uses this when read/write LValue is needed
public static ref T ItemRef(ref ValueArrayN<T, T1, T2, T3, T4> array, int index)
=> ref ValueArrayHelpers.ItemRefImpl<T, ValueArrayN<T, T1, T2, T3, T4>>(ref array, index);
// compiler uses this when readable LValue is needed
public static ref readonly T ItemRefReadonly(in ValueArrayN<T, T1, T2, T3, T4> array, int index)
=> ref ValueArrayHelpers.ItemRefReadonlyImpl<T, ValueArrayN<T, T1, T2, T3, T4>>(in array, index);
private static int _length = CheckAndComputeLength();
private static int CheckAndComputeLength()
{
Check<T1>();
Check<T2>();
Check<T3>();
Check<T4>();
return default(T1).Length + default(T2).Length + default(T3).Length + default(T4).Length;
}
private static void Check<U>()
{
if (typeof(U) != typeof(ValueArray1<T>) &&
typeof(U) != typeof(ValueArray2<T>) &&
typeof(U) != typeof(ValueArray3<T>) &&
typeof(U).GetGenericTypeDefinition() != typeof(ValueArrayN<,,,,>))
{
throw new InvalidOperationException("unexpcted instantiation");
}
}
public int Length => _length;
// Actual data is stored here
private T1 items0;
private T2 items1;
private T3 items2;
private T4 items3;
public T this[int i]
{
get
{
return ItemRef(ref this, i);
}
set
{
ItemRef(ref this, i) = value;
}
}
}
// range-checked indexing via ref math
internal static class ValueArrayHelpers
{
internal static ref TElement ItemRefImpl<TElement, TArray>(ref TArray array, int index)
where TArray : struct, IValueArray<TElement>
{
if ((uint)index >= (uint)array.Length) throw new IndexOutOfRangeException();
ref TElement firstElement = ref System.Runtime.CompilerServices.Unsafe.As<TArray, TElement>(ref array);
return ref System.Runtime.CompilerServices.Unsafe.Add(ref firstElement, index);
}
internal static ref readonly TElement ItemRefReadonlyImpl<TElement, TArray>(in TArray array, int index)
where TArray : struct, IValueArray<TElement>
{
return ref ItemRefImpl<TElement, TArray>(ref System.Runtime.CompilerServices.Unsafe.AsRef(in array), index);
}
}
} |
Here are some examples of how compiler would translate things like The tests use a simpler version of ValueArray with some helpers and validation missing since that is not necessary when testing compiler. |
There is some conceptual similarity with @davidwrighton suggestion - map arrays to some generic nested structs. The main difference is whether to have the contained data as actual fields and thus having VA_1, VA_2,.., VA_N types. vs |
I think the primary problem with those approaches is that it doesn't work with any I think the attribute based approach (like the one @jkotas or I suggested) might be overall better since it should work with any |
The approaches just try to varying degrees reduce the changes to the runtime and when possible reuse the mechanisms of type construction/equality that what we already have - i.e. generics. |
One thing to be aware of is that checking for the presence of an attribute on a field is actually a very expensive operation to do during type loading. For instance, checking for the presence of the ThreadStaticAttribute takes about 0.2% of the startup time of applications, and an attribute that had some effect on all field types (not just statics) would have a larger impact. It might reach as high as 0.5% of startup time sacrificed for this feature. If we can place the information about the size of the field into a type, then we won't need to do anywhere near as much checking, which can reduce the runtime costs when the feature isn't in use by a very large amount. |
@VSadov I actually started implementing something similar a while ago: https://github.com/losttech/TypeNum . How would you compare that to your approach? Any plan to publish Nuget package? |
Duplicate of #61135 |
Today, C# emulates fixed-sized buffers by emitting an explicitly sized struct with a single-field. This works decently enough for primitive types which have a constant size across varying platforms, but it does not work well for user-defined structs which may have different packing or different sizes across varying platforms/architectures.
This can be worked around by explicitly declaring a struct that contains
x
elements of typeT
, but this can quickly become overly verbose for fixed-sized buffers that contain many elements (e.g. 128 or even 1025).I propose we expose an attribute that the VM will recognize and which it will use to dynamically size the type based on the information given.
This will allow a user to define something similar to:
This would also be beneficial for the
fixed-sized-buffers
proposal in C#, as it would avoid the metadata bloat problem that exists: https://github.com/dotnet/csharplang/blob/725763343ad44a9251b03814e6897d87fe553769/proposals/fixed-sized-buffers.mdThe text was updated successfully, but these errors were encountered: