1
votes

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.
1
Which version of which dotnet? And why would you want to know?Henk Holterman
@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
@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
@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
@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

1 Answers

2
votes

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.