-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion: fixed-sized buffers #1314
Comments
How is this different from...
|
The latter is a struct which is 1025 bytes in size. The former is an inline array of 1025 elements, where each element is a DXGI_RGB struct instance. |
Shouldn't this public fixed DXGI_RGB GammaCurve[1025]; be public fixed DXGI_RGB[1025] GammaCurve; The array thingy |
No, fixed-size buffers use this syntax to distinguish them from standard fields. GammaCurve is a fixed-size buffer field, not a field having an array type. However, since a special type is always generated for such a field, why not introduce a new special type syntax public static fixed int[5] GetValues() => return default(fixed int[5]); This is effectively producing a by-val array. Then the syntax |
Yes. Fixed-size buffers are an existing language syntax (which this proposal is extending) and are different from normal arrays (which are heap allocated and tracked by the GC). The syntax difference ensures the two features (arrays and fixed sized buffers) can be differentiated and updated independently without worrying about possible ambiguities. |
@IllidanS4, see |
No, there is a difference between my idea and |
That would be a completely separate/distinct feature and would need its own proposal. |
How insane would it be to "just" allow consumers to use the existing Picturing... struct MyStruct
{
public fixed ulong Values[4];
}
class Program
{
MyStruct s = default(MyStruct);
Span<ulong> values = s.Values;
Console.WriteLine(values.Length); // 4
} Seems like "all it would take" (heh) is an attribute that tells the compiler the size of the field, and then the offset to the start of the buffer from the start of the struct could be either derived from metadata if sequential / explicit layout or computed live if auto layout (maybe at JIT time? I don't know how this one would work, sorry)... combine a Is this insane? |
Hmm, to answer my own question, without runtime support, that (edit: "that" = just exposing an easy safe way for callers to get Right? |
No, because there is nothing unsafe in the layout of the actual compiler-generated struct. Each element is represented by a separate field with the correct type, and since the whole struct is a field of the containing type, the runtime has perfect knowledge of all the references which may be stored inside. |
Sorry, my comment was a bit ambiguous. I was positing a problem with my own |
A lot of C++ libraries have support for specifying the size of fixed arrays using template arguments. The fixed arrays are still allocated on the stack, but the size is specified at compile time. For instance, the armadillo library has the |
@johnwason See [Proposal] Const blittable parameter as a generic type parameter - constexpr used for blittable type parametrization |
I've played around with working around these limitations in order to operate directly on blittable data structures. It becomes more work to maintain once you start nesting structures or arrays of structures, but it's functional. Simple example:
|
Hello why do you use Span? typedef struct
{
float X, Y;
} Vec2;
...
typedef struct
{
Vec2 vert[4];
float distFromCamera;
int planeIdInPoly;
} ScreenSpacePoly; In C#: public struct Vec2
{
public float X, Y;
}
...
public strcut ScreenSpacePoly
{
[FixedBuffer(typeof(Vec2), 4)] <- Error line shows....
public FixedBufferOfVec2_4<Vec2> vert;
[CompilerGenerated, UnsafeValueType, StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct FixedBufferOfVec2_4<T>
{
private T value;
public T this[int i] => i <= 4 ? Unsafe.Add(ref Unsafe.AsRef(in value), i) : throw new IndexOutOfRangeException();
public Span<T> Span => MemoryMarshal.CreateSpan(ref value, 4);
}
public float distFromCamera;
public int planeIdInPoly;
} Is it correct or wrongly? But how do I get limited index example: No more than 4? Thanks! |
This comment has been minimized.
This comment has been minimized.
What do you mean But I find horrible but I don't know how do I pass with But I want know that. |
The implementation of InlineArray is of little value, and it may even be unnecessary to implement it, since you can use a source code generator to generate a structure replacement yourself. The source code generator looks like this: StringBuilder sb=new();
for(int i=0;i<size;i++)
sb.AppendLine($" [FieldOffset(i*sizeOfStruct)] public {BaseStructType} p{i} {Enviroment.NewLine};")
var inlineArrayCode=@"
namespacexxx;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct Buffer_{BaseStructType}_{Size}{
{sb.ToString()}
}";
spc.AddSource($"FixedSizeBuffer.g.cs", inlineArrayCode); |
This is not strictly equivalent at the ABI level and may break for interop on some future platforms, where-as InlineArray is specially supported by the runtime to do the correct things. Additionally, it is missing the general language integration, optimizations, and other features that are present for InlineArray making it an overall worse option |
@tannergooding you are right. But I wish to have fixed struct in C# ( 13 language version ) but why I need to use |
@DeafMan1983 You have to do this. Yes it sucks and is verbose but at least it works. using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[InlineArray(4)]
struct FixedArray
{
private Vector2 element;
}
[StructLayout(LayoutKind.Sequential)]
struct Buff
{
public FixedArray a;
} |
The reason that simply Changing that meaning 20 years into the game would've been breaking, especially for the large amounts of foundational interop code that has existed and been in use up until this point. It's unfortunate, but that's simply how all programming languages work. You cannot simply break back-compat on a whim due to potential of large downstream breaks and other issues. The new The language is then free to get newer features in the future which may build upon |
But they are syntactically compatible. It can even be said that from a grammatical perspective, the C# designers took this into consideration from the beginning and reserved the possibility of implementation. |
But not semantically. We don't want literal identical syntactic constructs to have different semantic meaning like this. We considered that as part of the work, and quickly determined there were too many problems going down that route. |
This reminds me of a problem that I have not understood for a long time. Custom unmanaged structs are obviously similar to cl-predefined structures (int16, int32...), but why does the compiler prevent them from being defined as constants? In other words, the compiler does some special processing for them. But why can’t this special treatment be smoothed over and treated equally? Make custom unmanaged structures first-class citizens. |
I'm not sure what the semantic difference is. |
How do I pass with Array of Vector2 or Pointer of Vector2? [InlineArray(4)]
public struct FixedBufferVec2_4
{
private Vector2 vert;
}
public struct ScreenViewport
{
public FixedBufferVec2_4 vert;
}
static void Main()
{
Vector2[] verts = [
new Vector2(-40, 40),
new Vector2(40, 40),
new Vector2(40, -40),
new Vector2(-40, -40)
];
ScreenViewport svp = new()
{
vert = verts // still error
};
fixed (Vector2* vertptrs = verts)
{
svp.vert = verts; // still error, too
}
} How do I know like I communicate with FixedBuffer? You mean I need add Span and ReadOnlySpan? |
Because from the language level, they are not constants. Someone would have to spec out what this means at the lang level in order for the compiler to allow it. That hasn't been done yet. Why? Because no one has done it. :)
From the language level it's totally fine for reference types to be constants.
Because no one has written a spec for how that would work. We'd need to start with a discussion on the lang design there. Then it would need to be championed. Then a good spec would have to be made. Then it would have to get implemented. Nothing is stopping this, except the enormous amount of work above. |
Drawing on the development history of other languages, once a language deviates during its development, it must either turn the tide and correct it as soon as possible, or keep the error and continue to patch it. Keeping bugs + patching = means counterintuitive. Although C# is rich in features, it also has many implicit rules. Most of them are issues left over from history. |
@sgf I don't really have any idea what you're asking for :) We have no intent on breaking the world with our new language versions. At the same time, we are continuing to improve and develop on the features we have (including iterating on ones recently shipped). If there are changes you want to see, please open discussions on them and we can follow the process we have for doing language development and improvements. |
Whether this feature is implemented or not, I no longer care. Thanks to @CyrusNajmabadi for answering all my questions.
Unfortunately, the warehouse has not yet been fully tested and is only a prototype. A key part of the repository: the use of the source code generator was completed under the guidance of @CyrusNajmabadi. Thanks to him for teaching me how to use source code generators. |
To be a little blunt here, there isn't an opinion on this topic and you're fundamentally incorrect. Every single architecture (such as x86 vs x64 vs Arm32 vs Arm64 vs RISC-V, vs LoongArch vs PowerPC vs ...) and Operating System (such as Windows vs Linux vs MacOS vs Android vs iOS vs ...) has what is known as an The base platform ABI is typically defined by and around C and every other language in the world must interop with this base ABI in some fashion. Higher level languages, including C++, which have additional concepts may then expand that ABI with additional details/semantics on how they behave (this can include exception handling, memory handling, initialization of global or static state, etc). Some languages largely don't care about interop with the base platform ABI and go and do effectively their own thing. This can be particularly prevalent when working with higher level language constructs. Other languages care deeply about interop and utilize this to allow what is typically fast and near seamless integration between the higher level language and the base ABI. C# tends closer to the latter camp and is itself designed not only to be familiar to the rest of the C family of languages, but also to allow easy interop with C. Types like Once you start getting into user-defined structs, you are composing multiple primitive types together and thus you start having to get into additional considerations around layout that can not be static and thus their size cannot be constant. For example, there are common ABIs where Likewise, there is a difference between There are also cases like Effectively, there are a ton of rules that exist here and C# needs to support targeting platforms, like IL, where you might have an abstract virtual machine and where the actual layout is computed later (such as by the JIT, AOT, or even an object linker). Even C/C++ often operates the same way with its compilation process as the source files are typically compiled to intermediate objects and then linker takes in those object files and may do final layout computations and some architecture specific optimizations, trimming, etc. So, C# can't just go and make The decision ended up with where But ultimately, there is always some subset of users that do this for every feature (and often different users each time). Even some of the most loved and uncontroversial C# features have had people coming in and complaining about them and saying it was done wrong. However, overall the language design team makes good decisions and the language continues to grow in popularity, general user love, and places it can be used. So they are likely most things consistently right ;) |
I think languages should define their own standards system independently of the platform. When I send network data using little-endian data on MAC-OS, I don't need to convert it to big-endian because my sender (C# MAC-OS App) and receiver (x86WinServer) are both little-endian. Of course as you said, the ABI needs to be taken into account when appropriate. This should be done at the OS-API interaction layer for interaction and encapsulation. But the interaction and encapsulation here should not affect the structure definition inside the language (of course, if the performance problem is caused by io-intensive operations, it needs to be dealt with specifically, such as directly defining ABI-compatible structures inside the language and operating with them. , which should also leave customization capabilities for such scenarios). Knowledge points related to assembly such as registers. I'm really not know much about this. As far as I know, on x86, the compiler can decide how to passing parameters Because I know that Delphi and VC use registers for passing parameters slightly differently (it seems that Delphi uses registers, while VC uses push and pop), so I can conclude that if it is not necessary to expose interaction points (for example, on the windowsx86 platform In the case of exported functions), the compiler and language internals can maintain their own architecture. Of course, I'm not sure if this involves the underlying complexity of the compiler compiling IL to asm.I don't know enough about the underlying content, thanks for the guidance. |
Like I mentioned, some platforms do this. Even C#/.NET do this for some concepts that have no common mapping in C, like shared generics. However, this is typically limited to only concepts where that's required and I'll go into that a bit further down.
I think you grossly underestimate the overall impact of deviating from the underlying platform. Any call into native must make a transition and fixup the handling. This can and typically does happen for almost any underlying operation including things like:
Thus, a typical runtime is typically making many of these transitions per second and the overhead can rapidly add up and lead to pessimizations of your code in terms of overall usability and perf. Additionally, you are more likely to run into fundamental incompatibilities with the underlying architecture and your code actually becomes less portable by trying to make it more stringent.
You're making an assumption about the sender/receiver here and in practice cannot do that. There are typically dozens of machines involved with any network request and that need to inspect packet headers to ensure the data is passed from point A to point B correctly and efficiently. We define a fixed standard because that ensures all machines interacting with this data can understand it correctly. We define it in a way that allows efficient interaction of the data with more primitive hardware. We define it in a way accounting for the overhead of network transfers and understanding that the minimal changes to fixup byte ordering are "free" on most modern CPUs and can be done as part of the load/store operation, so the endianness conversion is typically a non-issue.
As indicated above, the cost of this is non-trivial and incredibly frequent. Even for cases where you simply need to change the registers around, you're incurring a cost per argument/return value in order to shuffle everything around given typically limited register space. When you start talking about composed data structures, you're now going to incur a cost Unlike something such as endianness fixup, the cost of shuffling around registers and touching memory has a real cost and that cost rapidly adds up. It can also confuse the CPU, which often has support for more typical coding patterns, and lead to less efficient execution of your code (energy and perf-wise).
Not really. There are some cases where a non-public leaf method (that is a method which calls nothing else) with few callers may get specialized. However, this also breaks debugging, stack traces, and other considerations so it's not typically done. The perf benefits of it are also incredibly minute. Some platforms, particularly legacy C code on 32-bit x86, do have multiple calling conventions available ( Now, it would be possible to define, for the purposes of constants over user-defined types, a fixed layout for the IL definitions of such data. But that must then be paired with a special runtime API that the compiler can defer to so that data can be recognized and fixed up on any platforms where the layout differs. But, that is very different from treating something like |
If I understand correct then ABI works and understands functions like 00000011 like Binary Hexadecimal. Of course fixed types like float,double, bye and more ( only numeric types ) are easy format. Like @tannergooding said. Example: in C style: For C# style: You need to create structure with Imagine about typedef {
HWND wins[4];
int n_wins;
} AllWindows; In C# public unsafe struct AllWindows
{
public fixed HWND wins[4];
public int n_wins;
} If C# Analyser doesn't like it than we use [InlineArray(4)]
public struct FixedBufferWins
{
private HWND hwnd;
// ... ToSpan etc
}
public unsafe struct AllWindows
{
public FixedBufferWins wins;
public int n_wins;
} Thanks for explanation of longer texts. |
The one thing that makes zero sense to me is why this approach wasn't automated in a way that makes sense to 99% of people. Older compilers would fail to compile new code as has always been the case and no IL changes needed ect. Same approach just with syntax sugar (every wants btw) making it WAY more practical and less confusing syntax. It also encourages its use. I knew this was going to happen. The new syntax expects you to understand underlining runtime details which is horrible. I like understanding how things work too but man I only have so much time to learn every little cork in something because of legacy IL flaws. I do encourage you to consider adding syntax sugar to your solution here. Also I think you're (ie C# devs) assume a chicken and egg problem here. You think people don't use it because it doesn't exist (so how could they) and now it exists in a way thats not useful for anything but the rarest of things because its out of phase with code when in reality this type of optimization is desired in many cases not just interop. There have been many cases where just having a fixed array of classes on the stack or heap would be useful (forget interop). The approach now only continues to encourage allocations in situations otherwise not needed because the effort needed is annoying and clutters things up. Anyway everyone knows who I am. I've voiced this for years. Voicing it again because I'm clearly not the only one here that thinks this. Stuff like this in C# is what separates it from other managed langs giving it a balance of performance with practicality / productivity. In this case its kinda failed IMO. |
Because there would be too many places where the fact that they aren't arrays would cause problems: https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-10.md#fixed-size-buffers That doesn't preclude that it may be considered again, but until those issues can be smoothed out, they're not going to introduce a syntax that only creates more confusion and friction. |
The topics of why it was done this way was also covered in depth on the various issues, discussion threads, and specifically in depth with you on the .NET Evolution discord. There are future directions the language desires to go and this feature is the effectively raw backing runtime feature that makes them possible. Doing "more" around the feature, rather than the minimal support up front (much as is done for other core runtime features like say |
ALl of this was documented. And you have been involved in discussions going over the aspects here. Furthermore, we've made it clear we're interested in continuing investments in this area, but that we needed the core support there to make many important scenarios possible, and to lay the foundation of that future work. You are seemingly acting as if the intent on our part was to only ship in the current form and never do anything else here. Hopefully stating this here, as well as the other forum locations will make this clear :) |
Ok guys, I know I'm annoying. But you need to realize you're literally in these talks 24/7. This isn't my day job as I've said before. Lots of people are confused in different aspects here for an array of reason. I'm just more vocal about this. Most devs I know just move onto a different lang for reasons like this but I see C# as still the most useful lang today in my life. So sorry if I cry about it. And please understand most of what is translated to me over the years that comes in every so often in little bits on this topic is something along the lines of (solution found no reason to pursue further or its not a feature worth looking into to much more because no one cares, its a niche etc). Thats changed since years ago and I understand I don't know all the little runtime details making this annoying for you guys to tackle at all. However I'll read arguments for why something can't be done and it reads like its impossible to improve it or it just sounds fundumentally wrong to me because I'm missing yet another complex legacy IL or runtime flaw or complexity I just haven't thought about. If I'm wrong here I'm sorry for what comes of as pestering but again I can't read peoples minds and have to make some level of deduction when I read new information the way things are said. I'm sure I've earned myself a dark star of disapproval here but I've dug a hole far enough a couple more scoops isn't going to matter. With the links @HaloFour shared what does "if a public fixed size buffer shrinks" mean? How would they shrink? Or does this just mean GC collected? |
Honestly, please consider that your personal experiences may not at all reflect anything about the greater programming community. We do this sort of analysis, and we're ok with the plan here and how it is being executed. I get that you don't like it and you want different things, but continuing to harp on it, and continuing to ignore the very real and relevant reasons given for why things were done this way is not productive.
Instead of crying about it, please just constructively contribute to the future design discussions that overlap these areas.
No one is asking for that. But it's not constructive to continually act as if there was no thought or information provided as to why things were done a particular way, after numerous conversations on the topic. We're continually open and transparent about the decision making that went into this. Including for the short and long terms. You may not like that reasoning, but rejecting it out of hand does not serve any sort of constructive purpose.
The code is updated to have a smaller fixed size buffer. This clearly would have significant downstream impacts on consumers (who have hardcoded knowledge about that length. |
100% however most people I know and have worked with don't interface in the "community" withholding their feelings. To the point of it being detrimental to a product like bug reporting something they need fixed. I tend to bug report issues on products I use.
Understood. I really do apologies for this. I really should hold back more than I do sometimes.
Some info I've gotten in the past was not consistent and initially this feature came off as a hard no back in the day. Maybe that stuck with me wrong IDK. Anyway thanks for your work. You guys really are a good team for all my distasteful criticism I give. |
Introduce a pattern that would allow types to participate in
fixed
statements.LDM history:
Tagging @VSadov @jaredpar
The text was updated successfully, but these errors were encountered: