Support sizeof(x) where x is a fixed buffer #8784
Replies: 46 comments
-
So this is what developers have to write if they want to try and maximize performance of code using fixed buffers (which are often used mainly for performance reasons!) public static class FixedBuffer
{
private static ConcurrentDictionary<string, int> buffer_lengths = new ConcurrentDictionary<string, int>();
public static int GetCapacity<T>(string FixedBufferName) where T : struct
{
var key = $"{typeof(T).Name}.{FixedBufferName}";
if (buffer_lengths.TryGetValue(key, out int Capacity))
return Capacity;
var fields = typeof(T).GetFields().Where(f => f.Name == FixedBufferName);
if (!fields.Any())
throw new ArgumentException($"No field with that name exists within the type: {typeof(T).Name}", nameof(FixedBufferName));
var args = fields.First().CustomAttributes.First().ConstructorArguments;
buffer_lengths.TryAdd(key, (int)args[1].Value);
return buffer_lengths[key];
}
} Can we therefore consider adding Since fixed buffers have a very contrived type definition (I personally think fixed buffers should be implemented as a kind of stand alone primitive type) its not possible to pass a type name into sizeof so the declared buffer identifier makes sense, in this case. |
Beta Was this translation helpful? Give feedback.
-
In the same scope... that wouldn't require a lookup as you created the buffer in that scope? You know it's size... |
Beta Was this translation helpful? Give feedback.
-
True but we must repeat the value everywhere we use that size creating fragility should anyone alter the size in one place and not others. Once can create constants and so on to address this but since its size is fixed compile time constant it strikes me as easy to deliver this. |
Beta Was this translation helpful? Give feedback.
-
Really, it seems like the fragility comes in the code which consumed it... Provide an example IRL where this occurs as I can't even contrive it...
One would simply refer to mySize. |
Beta Was this translation helpful? Give feedback.
-
Take a look at this code (just posted here): public interface IFixedBuffer
{
int FixedBufferLength { get; }
}
public static FixedBufferExtensions
{
public static ToString<T>(this T buffer) where T : IFixedBuffer
{
var length = buffer.FixedBufferLength;
...
}
}
using System;
public unsafe struct AString_32 : IFixedSizeBuffer
{
public fixed char chars[32];
public ReadOnlySpan<Char> AsSpan()
{
fixed (char* c = chars)
{
return new ReadOnlySpan<Char>(c, 64);
}
}
public int FixedBufferLength => 32;
} The 32 appears twice (actually three times), the 64 appears once, changing one necessitates changing the others. Then imagine you have many versions of this kind of type and the fragility becomes more apparent. This could be coded as: public interface IFixedBuffer
{
int FixedBufferLength { get; }
}
public static FixedBufferExtensions
{
public static ToString<T>(this T buffer) where T : IFixedBuffer
{
var length = buffer.FixedBufferLength;
...
}
}
using System;
public unsafe struct AString_32 : IFixedSizeBuffer
{
public fixed char chars[32];
public ReadOnlySpan<Char> AsSpan()
{
fixed (char* c = chars)
{
return new ReadOnlySpan<Char>(c, sizeof(Char) * sizeof(chars));
}
}
public int FixedBufferLength => sizeof(chars);
} This may seem to be grumbling BUT the buffer size is a known compile time constant, so why not? |
Beta Was this translation helpful? Give feedback.
-
@Korporal public interface IFixedBuffer
{
int FixedBufferLength { get; }
}
public static FixedBufferExtensions
{
public static ToString<T>(this T buffer) where T : IFixedBuffer
{
var length = buffer.FixedBufferLength;
...
}
}
public unsafe struct AString_32 : IFixedSizeBuffer
{
public fixed char chars[FixedBufferLength];
public ReadOnlySpan<Char> AsSpan()
{
fixed (char* c = chars)
{
return new ReadOnlySpan<Char>(c, FixedBufferLength * 2);
}
}
public int FixedBufferLength => 32;
} |
Beta Was this translation helpful? Give feedback.
-
I don't follow:
|
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt - That cannot compile - the fixed buffer bounds [ ] must be a compile time constant. |
Beta Was this translation helpful? Give feedback.
-
The code is sample only (it won't compile for other reasons) but post what you mean? |
Beta Was this translation helpful? Give feedback.
-
@Korporal public unsafe struct AString_32 : IFixedSizeBuffer
{
public fixed char chars[LENGTH];
public ReadOnlySpan<Char> AsSpan()
{
fixed (char* c = chars)
{
return new ReadOnlySpan<Char>(c, FixedBufferLength * 2);
}
}
public int FixedBufferLength => LENGTH;
const int LENGTH = 32;
} |
Beta Was this translation helpful? Give feedback.
-
The point I'm making is not that there are no alternatives but that the length/capacity of a fixed buffer is a fixed compile time constant and in this case it makes sense to be able to treat it as we can other types that also have a fixed compile time size. In other words why is a fixed buffer the only fixed sized type that cannot be used with sizeof? |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt - Of course that is an option, but we don't have to do this for other fixed size types. |
Beta Was this translation helpful? Give feedback.
-
@Korporal this also works but I would likely use the pattern @YairHalberstadt showed since as you stated the value is known at compile time.
|
Beta Was this translation helpful? Give feedback.
-
Until one adds other fields to the struct... |
Beta Was this translation helpful? Give feedback.
-
@Korporal, if I added more fields my code would reflect the mathematical paradigm to allow the offset to be calculated e.g. offset-of or otherwise. |
Beta Was this translation helpful? Give feedback.
-
@juliusfriedman Not sure I understand you Julius. The |
Beta Was this translation helpful? Give feedback.
-
@Korporal, not sure how to further explain it... @HaloFour, I believe that is a much better approach! I am also pretty sure there is a proposal for that as well. |
Beta Was this translation helpful? Give feedback.
-
Yes. It's called: a LDM member wants to champion this. Meaning, they've assessed all of that and made such a determination. |
Beta Was this translation helpful? Give feedback.
-
Agreed. I would definitely support such a proposal if it were created. |
Beta Was this translation helpful? Give feedback.
-
Implicit or explicit conversion to |
Beta Was this translation helpful? Give feedback.
-
Should a proposal be created at least to track the work to add it to that other proposal? Otherwise we're just all relying on someone remembering to bring it up at some LD meeting? 😁 |
Beta Was this translation helpful? Give feedback.
-
Out of curiosity where approx in the Roslyn source is the parsing and validation of Just curious about what I would need to change if I were to (experimentally) implement this in a test build of the compiler. Thx |
Beta Was this translation helpful? Give feedback.
-
Hey @Korporal . Parsing is handled here: Semantic analysis and binding of that expr happens in: Actual handling of it in terms of IL seems to happen here: I hope this helps! |
Beta Was this translation helpful? Give feedback.
-
Because the compiler doesn't know the size the type is outside of some basic types. There is no relation between the IL you compile against and the IL you run against, so the type may have a totally different size at runtime. |
Beta Was this translation helpful? Give feedback.
-
OK I understand - to an extent - (platform agnostic). But surely a Byte is universal as is Int16 and so on? I personally don't know of any hardware that has a byte that isn't 8 bits or a 16 bit signed it that isn't 16 bits, am I missing something? |
Beta Was this translation helpful? Give feedback.
-
At least as far as the C# spec is concerned, the various primitives are constant and are handled that way: https://github.com/dotnet/csharplang/blob/master/spec/unsafe-code.md#the-sizeof-operator However, all other structs are just handled via the |
Beta Was this translation helpful? Give feedback.
-
@tannergooding - Thanks for clarifying! |
Beta Was this translation helpful? Give feedback.
-
As mentioned: Because the compiler doesn't know the size the type is outside of some basic types 'byte' would be one of those basic types. |
Beta Was this translation helpful? Give feedback.
-
Weird... why did my post show up so much later... |
Beta Was this translation helpful? Give feedback.
-
I just had a real-world case where this would have been useful:
Here,
The intention of this Also, any changes to the size of the buffer would no longer require changes in two places. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm astonished that
sizeof(x)
doesn't compile when x is a fixed buffer that's within scope. Since such buffers have compile time constant sizes it should be almost trivial to implementsizeof
here, can this be considered?The only way I can see is to reflect upon the containing type, get the field with the name of fixed buffer field, get its
FixedBuffer
attribute, get that attributes constructor args and eventually get at the declared length!Such code (even if done statically once) is very cumbersome and fragile (should MS ever alter that
FixedBuffer
attribute or whatever). I can use a static dictionary keyed on containing type and fixed buffer field name and cache the length there (the first time its requested) but even so a dictionary lookup for a simple integer constant that's fixed and known at compile time? really?(At least
nameof
works as expected! butsizeof
andtypeof
give rather unhelpful error messages)Beta Was this translation helpful? Give feedback.
All reactions