-
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
Discussion: Stack stored classes, Heap stored "value classes" #460
Comments
And I do not see how emoticon reactions and unargumented statements like the the one above answer the quesion and lead to a serious discussion! 😞 I ask a further question, and I would be lucky to get real answers on that! How high are the costs of creating a class object on the managed heap, instead of creating an identical struct instance on the stack? |
This would certainly require a CLR change. Even C++/CLI and MC++ are unable to allocate .NET classes on the stack. For reference: dotnet/roslyn#2104 |
@HaloFour Thanks for the valuable links 😃 On my processor the runtime difference of an optimized compilation between allocating a class (without any field initializations) and an identical struct is between 10:1 and 20:1. |
ClassBar firstBar = new ClassBar();
ClassBar #secondBar = new ClassBar(); When the compiler reaches the second I don't see your point on nullables. |
Stack allocation is certainly much faster. But it comes with a lot of limitations, especially if the C# compiler is to attempt to enforce safe memory management. I think that the cases that this feature could be applied would be incredibly limited as a result. You couldn't take an arbitrary class and just "pin" it to the stack. The class would likely have to meet a very strict set of guidelines that would have to be enforced by the CLR. For example the class might have to be Then you have the entire problem of escape analysis. That reference can't go anywhere since it points to a portion of memory that is unmanaged. I'm not entirely sure how that can be adequately handled since effectively calling any instance method on the reference is a form of escaping since those methods could save |
@lachbaer Objects on the heap are freed by the GC, which ensures that the object survives until after the point when its last reference is no longer reachable. You have not explained to us how you would accomplish that for objects that are on the stack, or what the behavior should be when there are dangling references. So we cannot answer how expensive it would be. |
@gafter What I mean is, that once the stack is cleared, the object is immediately finalized. To ensure that no dangling references occur, the stack object gets a new type name suffixed with a Also, an "object# obj = struct Object();" is by default converted to a The object on the stack is not a reference, it is a copy. It's like A valid issue is when the finalizer frees resources, that are pointed to by reference fields of the object. There can be dependencies between the objects that could lead to undecicive behaviour. |
You have to be able to take references to the copy on the stack in order for instance methods to work. |
@HaloFour Seems that I have a lack of knowledge here. What is the difference on how instance methods of structs work compared with reference types? You can use |
In the case of struct instance methods the compiler pushes a |
@HaloFour But that means that the |
So you're suggesting that the C# compiler should create a struct type and copy the reference type's fields and methods? |
@mikedn I think that there must be a CLR change for this and that the CLR does this in the background. The compiler creates the type only as a TypeSymbol. |
@lachbaer What would happen in the following code? class C
{
private static C storage;
private int state;
public int M()
{
storage = this;
storage.state = 42;
return this.state;
}
}
…
C c1 = new C();
Console.WriteLine(c1.M());
C# c2 = struct C();
Console.WriteLine(c2.M()); As far as I can tell, there are two options:
What am I missing? Is there some third option? Or do you think one of the two options above is acceptable? |
What's more the IL for both the |
Is it only in terms of memory or are classes allocated on stack passed to a method by value as well? Can parameter of type If not you won't be able to use it with any existing code. Otherwise you need to get reference, which you stated shouldn't be possible. |
Can you submit an example? You are probably right, but I haven't seen solvable differences in the IL. A CLR change must be done for this anyhow.
I admit not to think of self-references. 😞 That will crash whatever way I think. But I weren't me, if I hadn't had a solution in my mind 😀 : The purpose of this construct in the first place is to allow a relevant performance boost for lightweight classes, who are classes and not structs for some reason. Most of those classes I can think of are part of my projects, so I have access to them, or will probably be lightweight classes of the framework, that could be rededicated easily. If backward compatibility is no issue - because until now we dealt with the current situation somehow - a new "kind" of public class# Person { ... }
public class# Student : Person { ... } Some assurances must be made about this classes, two being
This means, that not per se all classes can be put on the stack, but we would have the opportunity to create new classes for which this feature would actually (and only) make sense. (Besides we get the benefit of an implicit "Dispose" by a deterministic called finalizer.)
Yes, it will be shallow copied on the heap, loosing its performance benefit. The method then operates on a copy of the object! But there is a solution to that, too: void RenameStudent(Student stud) { stud.Name += " the douche"; }
Student# student = new Student#("Eric");
RenameStudent(student ref); After |
Not "can", "must". There is no other way to ensure that a reference type allocated on stack doesn't use
So you're proposing that significant changes are made to the runtime and the language in the name of performance but it all falls flat on its face as soon as the usage becomes slightly more complicated. It's probably cheaper and more effective to let the runtime do escape analysis and stack allocate reference types that do not escape the stack. |
When looking at all the issues I have posted upon recently - of course including this one 😁 - it kind of seems to me that we/I treat the symptom, not the cause. With acceptable CLR changes being unavoidable at least thinking about a completely new native type, not being backwards compatible, is acceptable. I'll call it It will bring several things together
That Addendum: to facilitate some of the characteristics of that new type, an additional, specialized heap with less CLR overhead can be created in memory. The stack then simply stores the pointer. |
Sounds like a massive amount of work for very little real benefit. |
@HaloFour The primary benefit lies in performance. In very many cases you operate on midweight objects that are already too heavy for being structs but don't make much or any use of comprehensive class features. Nevertheless you finally decide for The CLR is initially designed to support the broad OOP feature of classes, but the costs for that managed behaviour is massive. Performance measures underline that and for some this is the contra-argument to not go with .NET, Java or alike. With this, admittedly big, addition to the CLR many often used (custom) classes nearly get the performance of |
What makes object too big for being a struct, but not too big for stack allocated class? |
And as already stated there are alternatives to this proposal that are likely easier to implement. |
That's why we have |
I continue to think when I read these genre of issues (stackalloc, destructible types, this "strass") that there is fundamental fallacy: C# is NOT C/C++ and I don't want to have to preoccupy of the memory management! This is should be the work of the Jitter / AOT compiler doing the correct escape analysis and then allocating objects on the stack only when it is safe to do; the objection that is impossible does not make sense: Java has this from years and the JVM and the CLR are really a lot similar! Stack analysis will give this all for free:
In the end the boundary between ValueTypes (technically an "hack") and Objects will become indeed totally nonexistent an hypothetical new .Net could not have this distinction and be a pure object oriented language (as Java would wanted to be but then they feel the necessity of "primitive types" and in some way broken it) in which Integer is an Object but 99% of the times is allocated on the stack as if it was a "native" int. |
Please stop with the portmanteaus, lol... |
Not really. For one thing escape analysis is limited in what it can do (e.g. it's very difficult to allocate on the stack objects that are returned). And more importantly, being a value type and being allocate on the stack are independent things.
Value types have nothing to do with C# being or not being a "pure" OOP language. Not to mention that the idea of a "pure" OOP language is archaic. |
Then use a class with value semantics. We have tons of them all over the place. The most classic example is "System.String". Again you haven't been able to explain why existing solutions are not sufficient. If you want this sort of thing then you can write such a thing using an attribute + a roslyn analyzer. If you can't explain to me why that's insufficient, and why this requires a language change, then i'm not going to have any reason to feel like we should add this. |
Haeh??????? What am I doing here the whole time....????????????????? I stop now. |
@lachbaer Please stop giving people thumbs-downs for asking you for the situations where this would be valuable. If you are proposing language changes, then the onus is on you to be able to provide clear and suitable justifications for why those language changes are worthwhile. Right now you still haven't been willing to answer my question about why you couldn't accomplish this today with just an attribute and an analyzer. You also haven't put forth a compelling scenario where i woudl want this. Your fraction example is actually a really poor example to me. If i had a fraction type, why would i want to be forced to take an allocation hit on every assignment? That seems like a very bad way to design this API. If you want me to be convinced of your position, you need to provide an actual use case where this sort of enforced behavior is desirable. |
Having language support for #= forces a pattern. It is a bit like introducing 'type?' for Nullables. You don't actually need it, you could use the Nullable methods, like in the early days of VB. Or a bit like '??'. You could express that differently (besides the performance benefit). I think '#=' fits in the same category for the reasons mentioned in the previous paragraph. |
That's not actually the case :-) There were necessary changes to the runtime to make Nullable work that could not be achieved with actual pure language support. :) For example, say that Nullable<int> i = default(Nullable<int>);
object o = i; What is 'o' here? If 'Nullable' is just a normal struct, then 'o' would actually be a boxed version of that struct. But that's not the case. |
@lachbaer Would you be willing to discuss any of this offline? You can reach me at cyrusn@microsoft.com. thanks! |
Don't take me too literally on the Nullables. I meant things like "if (x.HasValue)" instead of language supported "if (x != null)". Also '#=' would give a programmer the ability to just have a custom assingment operator. In the guides is stated that you cannot overload the standard '=' assignment operator. Well, if actually nobody asked for that or tries it then you delete that statement from the guides and save that digital ink 😉 |
@CyrusNajmabadi I'll write you. But know I have to leave the PC. Good night. (03:21 here). |
Of course, I haved used that already sometimes 😱 Just didn't think of using it in my Fraction above. |
@lachbaer Any opportunity to talk? |
@CyrusNajmabadi You've got mail... |
Idea update: |
I'm totally lost with how it evolved from stack allocated class through heap allocated struct, couple of other stages I'm not sure anybody understood, into Roslyn analyzer and finally syntactic sugar for However I do not like the idea of new operator. Especially if it does three different things based on implemented interfaces and constructors and only works for quite limited subset of types. More obfuscation than help to me. |
Agreed with @dstarkowski , this whole proposal has gotten messy. I'm starting to think that support for Default Interfaces + Parameter-less Struct Constructors should be sufficient to meet the needs of this proposal. That is, if they get accepted and implemented. |
I admit that it got very clumsy all the way down. The discussion about parameterless constructors (#99) in structs and it's vast attendance - my interest in it, too - made me think about alternatives that give the users what they intend with it.
The only way to achieve that would be a completely new type class ('strass' 😉 ). But for sure the immense work to the CLR that this would imply is most probably not worth it. Hey, we lived without it 15 years already. 😁 That is why I finally came up with the idea of creating something like an assign-by-value operator. Of course that is only a compromise, but may help using a class more struct like and guard agains typical errors that arise when accidentially assigning a class by value instead of cloning it. That's the whole point. We can define mathematical (-like) operators on classes, yet we cannot define a mathematical-like assignment operator. We must either use a To me that makes much sense. Maybe it needn't be as complicated as stated above. |
If I hear the word strass one more time... 😝 |
Is it strassing you out? |
Sometimes a possible solution to value-type-classes can be easy, in terms of syntax additions. Why not simply defining a public struct class Fraction {
public double Numerator = 0d;
public double Denominator = 1d;
}
MyClass mc = new MyClass();
MyStruct ms = new MyStruct();
MyStrass msc = new MyStrass();
// or ... = default;
If such a type is published or referenced by other assemblies it must deal with nullability, because it is an actual CLR class. However other .NET languages can identify this type class by an attribute ('ValueClassAttribute') and make it a In generics it is internally handled like a standard class. But the calls to the generic functions are proxied and arguments and return values are cloned to a new instance, like with a standard struct. |
Because that's not self describing and confusing as all get out. Though, I do think that you're on to something here. Maybe a The other option (I keep pointing out) is to just make |
How do you propose that? The limitations imposed on structs are there for a reason; structs need to have a predictable size. Any capacity to actually inherit from a struct would have to be severely limited. |
@HaloFour I'm confused as to how inheritance would make the size of a |
If you have the following: struct A {
public int X;
}
struct B : A {
public int Y:
} How much memory does the CLR need to allocate in order to have a variable of type Now if the compiler/CLR considers |
Putting all theory besides. Does anyone has a serious real world example where value-type-semantics-only for a |
This would be a completely separate proposal but the compiler might be able to fake struct inheritance without any CLR changes: public struct A {
private int X;
public A(int x) {
this.X = x;
}
public int X { get => this.x; }
public void PrintIt() {
Console.WriteLine($"A.PrintIt: X = {this.X}");
}
}
public struct B : A {
private int Y;
public B(int x, int y) : base(x) {
this.Y = y;
}
public int Y { get => this.y; }
public override void PrintIt() {
Console.WriteLine($"B.PrintIt: Y = {this.Y}");
base.PrintIt();
}
}
// B is equivalent to
public struct B {
private A $base;
private int y;
public B(int x, int y) {
this.$base = new A(x);
this.y = y;
}
public int X { get => this.$base.X; }
public int Y { get => this.y; }
public void PrintIt() {
Console.WriteLine($"B.PrintIt: Y = {this.Y}");
this.$base.PrintIt();
}
} Not sure it's really worth it, though. |
@HaloFour Not much code saved, actually. Nevertheless, 👍 for the solution! I wonder, why the discussions on |
@HaloFour thanks for that, I was not considering the case of |
The discussion got quite messy all the way down. The only question finally left is whether there should be language support give special classes value type usage, e.g. with assign-by-value semantics. But that is another discussion. |
Fyi, work has started on stack-allocated objects. dotnet/coreclr#20251 |
For heap stored "value classes" please see #460 (comment)
Question
Why is it not possible to put classes on the stack? Value type
struct
s do not offer the comfort of classe swhen it comes to inheritance.In C this is possible by ommiting the
new
keyword.I can think of the reason, that accidentially ommiting
new
and creating a stack stored instance without any further notice can lead to unwanted behaviour. Also when creating the (first) CLR, strictly distinguishing between stack storedstruct
and heap stored and garbage collectedclass
might be easier.Motivation
There is ongoing development with (non-)nullable reference types, et alta. It seems that the borders between value types (
struct
) and reference types (class
) partly obliberate nowadays.I recently had scenarios where putting a
class
type on the stack to accelerate processing would be quite useful.Possible syntax
To create a class instance on the stack the following statement could be used, where
new
is replaced bystruct
.ClassBar#
marks innerBar as being not a pointer.To (flat) copy a heap stored instace to the stack, a
struct()
operator can be introducedImplementation possibilities
Alternatives
Maybe the practical performance impacts nowadays aren't as heavy, so that leaving classes on the managed heap is actually no issue any more.
The text was updated successfully, but these errors were encountered: