0

From this question I understood that

  • structs can be allocated on the stack or in registers and not on the heap
  • if a struct is part of a reference type object on the heap, the struct will also be on the heap

But how about a struct that is not part of an object, but a static member of a class like so:

public class Program
{
    public static CustomStructType inst1;
    
    static void Main(string[] args)
    {
        //assigning an instance of value type to the field
        inst1 = new CustomStructType();
    }
}

public struct CustomStructType
{
    //body
}

There will not be an instance of Program on the heap. So where will the struct be stored?

This question is a rephrased version of this deleted question. The user was deleted, so the question and answer went with it. I still found the idea interesting and the debugging result even more, so I chose to repeat it here.

About potential duplicates:

  • this question creates an instance of a class. As mentioned, I understand that structs stored as part of objects are on the heap. My code does not create an instance of a class.
  • this question leaves it open whether it's static or not, and the answer says "No, if you do that inside of Main, in general, it won't get allocated on the heap."
  • this question has a great answer by Jon Skeet, which says that every new allocates space on the stack.
trincot
  • 317,000
  • 35
  • 244
  • 286
Thomas Weller
  • 55,411
  • 20
  • 125
  • 222
  • 1
    Which version of which dotnet? And why would you want to know? – H H May 29 '21 at 22:30
  • @HenkHolterman: very valid questions. I guess OP of the deleted question didn't think it could be different in different versions. I hope that my answer is insofar version independent that it excludes stack and registers as valid storage places. Why would one want to know? For educational / learning purposes and getting the understanding right, I would say. I personally never cared about it in real life and never had problems :-) – Thomas Weller May 29 '21 at 22:44
  • @PeterDuniho: Eric Lippert mentioned it, but neither has a proof nor an explanation why that would be the case. And, as mentioned in the question, the code of the current duplicate creates an instance whereas the code of this question does not create an instance. – Thomas Weller May 29 '21 at 22:52
  • There's no need for proof. It's not guaranteed and so the question can't even technically be answered. A different implementation is free to do it differently, and for a given implementation, Lippert's assertion is all the proof anyone ought to need, but they can easily verify themselves if they really need to. I don't see what creating an instance or not has to do with anything; you're asking about static fields, so the instances of the class don't matter. – Peter Duniho May 29 '21 at 22:55
  • Go back to the fundamentals. (1) Variables are *storage locations*. (2) Storage locations can be short-lived or long-lived. By "short" we mean "not longer than the activation of the function that created the storage" (3) Lifetime of a variable has nothing whatsoever to do with its type; it has to do with for how long we know we need the stored information. – Eric Lippert Jun 01 '21 at 16:50
  • If the crux of your question is something like "is it the case that all storage created by Main could stop being alive when Main returns because the program ends?" then I would point out that (1) the compiler doesn't know that, and (2) it's not true. A finalizer running on the finalizer thread could access `Program.inst1` after `Main` returns, so the lifetime of `inst1` must be longer than the activation of `Main` and therefore `inst1` must be long-lived storage. – Eric Lippert Jun 01 '21 at 16:58
  • Similarly, a static constructor could access `Program.inst1` *before* `Main` runs. Again, the lifetime of `inst1` is longer than the activation of `Main` and therefore the storage must be long-lived. You can work these things out from principles; it's not clear to me what you mean by my failure to provide "proofs" or what would be a satisfactory proof. These are all implementation details and some implementations are open source. – Eric Lippert Jun 01 '21 at 17:05
  • 1
    @EricLippert: What you wrote in these 3 comments are very good, comprehensible reasons for static variables to live not on the stack and not in a register. That's enough of a proof. Eric, I know you and I know that you would never answer something you were not 100% sure about. – Thomas Weller Jun 01 '21 at 19:18
  • 1
    @EricLippert: The sentence you wrote in the linked answer was just a sentence there. It had do direct relationship to the original question. It's just there for correctness and completeness. And that's great. However, this question was specifically about static structs, so I wanted to not only have a correct sentence somewhere, but also provide at least one logical explanation of *why* it has to be that way. – Thomas Weller Jun 01 '21 at 19:19

1 Answers1

2

Perhaps you missed it: Eric Lippert has mentioned it in a side note:

[...] and static variables are stored on the heap.

That's written in the context of

The truth is that this is an implementation detail [...]

Here's how the Microsoft implementation does it:

But why are static variables stored on the heap?

Well, even the Main() method does not live forever. The Main() method could end and some other threads could still be running. What should happen in such a case to the struct? It needn't necessarily be on the heap, but I hope you see that it can't be on the stack and not in a register. The struct must be somewhere for other threads to still be able to access it. Heaps are a good choice.

Code example where Main() dies:

using System;
using System.Threading;

public class Program
{
    public static CustomStructType inst1;

    static void Main(string[] args)
    {
        new Thread(AccessStatic).Start();
        //assigning an instance of value type to the field
        inst1 = new CustomStructType();
        Console.WriteLine("Main is gone!");
    }

    static void AccessStatic()
    {
        Thread.Sleep(1000);
        Console.WriteLine(inst1);
        Console.ReadLine();
    }
}

public struct CustomStructType
{
    //body
}

Let's get back to your original code. When in doubt, you can always check with a debugger. This is a debug session of a Release build in .NET Framework 4.8 (4.8.4341.0).

I'm debugging with WinDbg Preview, which is a free debugger provided by Microsoft. It's not convenient to use, though. I learned about it from the book "Advanced .NET debugging" by Mario Hewardt.

I inserted a Console.ReadLine() for simplicity, so I don't need to step through everything and stop at the right time.

Load the .NET extension

ntdll!DbgBreakPoint:
77534d10 cc              int     3
0:006> .loadby sos clr

Search for an instance of Program (just to check whether the premise of the question is correct) indeed gives 0 objects:

0:007> !dumpheap -type Program
 Address       MT     Size

Statistics:
      MT    Count    TotalSize Class Name
Total 0 objects

Search for the class:

0:006> !name2ee *!Program
Module:      787b1000
Assembly:    mscorlib.dll
--------------------------------------
Module:      01724044
Assembly:    StructOnHeap.exe
Token:       02000002
MethodTable: 01724dcc
EEClass:     01721298            <--- we need this
Name:        Program

Get information about the class:

0:006> !dumpclass 01721298
Class Name:      Program
mdToken:         02000002
File:            C:\...\bin\Release\StructOnHeap.exe
Parent Class:    787b15c8
Module:          01724044
Method Table:    01724dcc
Vtable Slots:    4
Total Method Slots:  5
Class Attributes:    100001  
Transparency:        Critical
NumInstanceFields:   0
NumStaticFields:     1
      MT    Field   Offset                 Type VT     Attr    Value Name
01724d88  4000001        4     CustomStructType  1   static 0431357c inst1
                                                            ^-- now this

Check where the garbage collected heaps are:

0:006> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x03311018
generation 1 starts at 0x0331100c
generation 2 starts at 0x03311000
ephemeral segment allocation context: none
 segment     begin  allocated      size
03310000  03311000  03315ff4  0x4ff4(20468)
Large object heap starts at 0x04311000
 segment     begin  allocated      size
04310000  04311000  04315558  0x4558(17752)             <-- look here
Total Size:              Size: 0x954c (38220) bytes.
------------------------------
GC Heap Size:    Size: 0x954c (38220) bytes.

Yes, it's on the large object heap which starts at 0x04311000.

BTW: It was astonishing to me that such a small "object" (struct) will be allocated on the large object heap. Typically, the LOH will contain objects with 85000+ bytes only. But it makes sense, because the LOH is typically not garbage collected and you don't need to garbage collect static items.

Thomas Weller
  • 55,411
  • 20
  • 125
  • 222