9

Just out of curiosity - consider the following example:

public class A
{
    public static int Foo;
}

public class Program
{
    static void Main()
    {
        // The following variable will be allocated on the
        // stack and will directly hold 42 because it is a
        // value type.
        int foo = 42;

        // The following field resides on the (high frequency)
        // heap, but is it boxed because of being a value type?
        A.Foo = 42;
    }
}

My question is the following: is the value of the Foo field boxed because it resides on the heap? Or is it in a special container object / memory section that encapsulates it just like an instance value type field is part of an object on the heap?

I would assume that it is not boxed but I don't know for sure and I cannot find any documentation on it.

Thank you for your help.

feO2x
  • 5,358
  • 2
  • 37
  • 46
  • 1
    No it is not boxed. It will stay in heap as is. What makes you to think it may be boxed? – Sriram Sakthivel Sep 09 '14 at 10:06
  • Well, values of value types cannot reside on the heap on their own - except when they are boxed. When they are instance fields, they become part of the corresponding object and are not boxed but I couldn't find any info on what happens when they are static fields. – feO2x Sep 09 '14 at 10:08
  • 2
    @feO2x - Value types can exist on the heap without being boxed. Boxing is only required when you need to treat a value type as a reference type. – Lee Sep 09 '14 at 10:10
  • 1
    Reference Eric Lipert's Blog post on value types: http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx – coder_bro Sep 09 '14 at 10:11
  • @Lee thanks, I think you pointed out my fallacy. – feO2x Sep 09 '14 at 10:12
  • 1
    [This may help](http://stackoverflow.com/questions/1113819/arrays-heap-and-stack-and-value-types) – Sriram Sakthivel Sep 09 '14 at 10:20
  • @Ngm Thank you for the link, but unfortunately there is no real evidence in it what happens exactly in the common .NET CLR. – feO2x Sep 09 '14 at 10:21

4 Answers4

7

The CLR does not have the restriction that every field of a class needs to have the same storage type. Only instance members end up on the GC heap. Static members are allocated in the loader heap. Or in thread-local storage when the field has the [ThreadStatic] attribute. This of course enforces the contract that a static member is shared by every instance of an object of the class.

Very simply implemented btw, the jitter allocates the storage and knows the address of the field. So any load and stores directly use the address of the variable. There's no extra pointer dereference, very efficient.

So, no, there's no need at all to box, a static int will only occupy 4 bytes.

If you want to see this for yourself then use the Debug + Windows + Disassembly window. Shows the machine code, you'll see it using the address of the variable directly. It will be a different address every time you run the program, a malware counter-measure.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
3

As Sriram and Lee have given the answer in the comments of the question but did not provide an answer, I'll summarize the findings:

No, the value is not boxed. Value Types can reside on the heap, they are only boxed when they are used like a Reference Type.

You can also see that there is no boxing involved in the IL code of my example:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       12 (0xc)
  .maxstack  1
  .locals init ([0] int32 foo)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   42
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   42
  IL_0006:  stsfld     int32 StaticValueTypeFieldBoxing.A::Foo
  IL_000b:  ret
} // end of method Program::Main
feO2x
  • 5,358
  • 2
  • 37
  • 46
2

TL;DR: Yes, but not semantically, and only for non-built-in value types.

The following is based on my own reverse engineering of the internal workings of a CLR application.

 

The answers provided are not entirely correct, in fact, quite misleading.

This is an interesting one. It depends.

Built in types (Supported by the VES directly) like ints, floats, etc. are stored raw at the address of the static variable.

But interestingly, non-built in types like System.Decimal, System.DateTime, and user defined value types are boxed.

But interestingly, they actually get kinda, sorta... double boxed. Imagine:

public struct MyStruct
{
    public int A;
}

public static class Program
{
    public static MyStruct X;

    public static void Main()
    {
        Program.X.A = 1337;
        Program.DoIt();
    }

    public static void DoIt()
    {
        Program.PrintA(Program.X);
        Program.PrintType(Program.X);
    }

    private static void PrintType(object obj)
    {
        Console.WriteLine(obj.GetType().FullName);
    }

    public static void PrintA(MyStruct myStruct)
    {
        Console.WriteLine(myStruct.A);
    }
}

Now, this will work as you'd expect, MyStruct will be boxed for PrintType, and not boxed for PrintA.

However, Program.X does not actually contain the MyStruct instance directly like it does in an instance variable or a local variable. Instead it contains a reference to it on the heap, where the instance exists as an object with an object header and all.

As mentioned initially, this does not apply to built in types. So if you have a static variable containing an int, the static variable will take up 4 bytes. But if you have a static variable of a user defined type, eg. struct IntWrapper{public int A;}, then the static variable will take up 4 bytes in a 32-bit process and 8 bytes in a 64-bit process to store the address of a boxed version of the IntWrapper struct, where it takes up 8 bytes in a 32-bit process and 12 bytes in a 64-bit process (4/8 bytes for the object header pointer and 4 bytes for the int), ignoring any potential padding.

However, semantically it works like you'd expect. When calling PrintA(Program.X), the program will copy the struct part (the data after the object header) in the object pointed to by Program.X and pass that to PrintA.

When calling PrintType(Program.X) it indeed boxes the instance. The code creates a new MyStruct object with an object header, then copies the A field from the object referenced by Program.X into the newly created object, and that object is then passed to PrintType.

In summary, Program.X contains the address of a boxed MyStruct (If we define boxing as converting a value type to a reference type), but will still box (or clone) that object as if it was a value type, so the semantics remain the same as if it was stored in the static variable directly as a value type.

Like I said, I'm not sure why they're doing it this way, but they are.

I've included the JIT'ed disassembly of the C# code above and commented it. Note, I've come up with all the names in the disassembly.

A comment on the calls: All calls to managed methods happen through pointers. On the first call, the pointer points to the code that takes care of JIT compiling the method. After JIT compiling, the pointer is replaced with the address of the JIT compiled code, so any subsequent calls are fast.

Program.Main:
    MOV     EAX, DWORD PTR DS:[<Program.X>]                 ; Move the address stored in static variable Program.X into register EAX.
    MOV     DWORD PTR DS:[EAX + 4], 539h                    ; Set field at offset 4 (Offset 0 is the object header pointer) to 1337.
    CALL    DWORD PTR DS:[<Program.DoIt Ptr>]               ; Call Program.DoIt.
RET                                                         ; Return and exit the program.

Program.DoIt:
    PUSH    EBP                                             ; Function prologue.
    MOV     EBP, ESP                                        ; Function prologue.
    MOV     EAX, DWORD PTR DS:[<Program.X>]                 ; Move the address stored in static variable Program.X into register EAX.
    MOV     ECX, DWORD PTR DS:[EAX + 4]                     ; Copy the struct part (the dword after the object header pointer) into ECX (first argument (this)), essentially an unboxing.
    CALL    DWORD PTR DS:[<Program.PrintA Ptr>]             ; Call Program.PrintA.
    ; Here, the MyStruct stored in the static value is cloned to maintain value semantics (Essentially boxing the already boxed MyStruct instance).
    MOV     ECX, <MyStructObjectHeader>                     ; Boxing for PrintType: Copy the address of the object header for MyStruct into ECX (First argument).
    CALL    <CreateObject>                                  ; Boxing for PrintType: Create a new object (reference type) for MyStruct.
    MOV     ECX, EAX                                        ; Copy the address of the new object into ECX (first argument for Program.PrintType).
    MOV     EAX, DWORD PTR DS:[<Program.X>]                 ; Boxing for PrintType: Move the address stored in static variable Program.X into register EAX.
    MOV     EAX, DWORD PTR DS:[EAX + 4]                     ; Boxing for PrintType: Get value of MyStruct.A from the object stored in Program.X (MyStruct.A is at offset 4, since the object header is at offset 0).
    MOV     DWORD PTR DS:[ECX + 4], EAX                     ; Boxing for PrintType: Store that value in the newly created object (MyStruct.A is at offset 4, since the object header is at offset 0).
    CALL    DWORD PTR DS:[<Program.PrintType Ptr>]          ; Call Program.PrintType.
    POP     EBP                                             ; Function epilogue.
RET                                                         ; Return to caller.

Program.PrintA:
    PUSH    EAX                                             ; Allocate local variable.
    MOV     DWORD PTR SS:[ESP], ECX                         ; Store argument 1 (the MyStruct) in the local variable.
    MOV     ECX, DWORD PTR SS:[ESP]                         ; Copy the MyStruct instance from the local variable into ECX (first argument to WriteLine).
    CALL    <mscorlib.ni.System.Console.WriteLine(object)>  ; Call WriteLine(object) overload.
    POP     ECX                                             ; Deallocate local variable.
RET                                                         ; Return to caller.

Program.PrintType:
    PUSH    EBP                                             ; Function prologue.
    MOV     EBP, ESP                                        ; Function prologue.
    CMP     DWORD PTR DS:[ECX], ECX                         ; Cause an access violation if 'this' is null, so the CLR can throw a null reference exception.
    CALL    <GetType>                                       ; GetType.
    MOV     ECX, EAX                                        ; Copy the returned System.Type object address into ECX (first argument).
    MOV     EAX, DWORD PTR DS:[ECX]                         ; Dereference object header pointer.
    MOV     EAX, DWORD PTR DS:[EAX + 38h]                   ; Retrieve virtual function table.
    CALL    DWORD PTR DS:[EAX + 10h]                        ; Call virtual function at offset 10h (get_FullName method).
    MOV     ECX, EAX                                        ; Copy returned System.String into ECX (first argument).
    CALL    <mscorlib.ni.System.Console.WriteLine(int)>     ; Call WriteLine.
    POP     EBP                                             ; Function epilogue.
RET                                                         ; Return to caller.

 

Here's a comparison of the difference between built-in types like long and other value types.

public static class Program
{
    public static long X;

    public static void Main()
    {
        Program.X = 1234567887654321;
    }
}

Compiles to:

Program.Main:
    PUSH    EBP                                                 ; Function prologue.
    MOV     EBP, ESP                                            ; Function prologue.
    MOV     DWORD PTR DS:[DD4408], 3C650DB1                     ; Store low DWORD of 1234567887654321.
    MOV     DWORD PTR DS:[DD440C], 462D5                        ; Store high DWORD of 1234567887654321.
    POP     EBP                                                 ; Function epilogue.
RET                                                             ; Return.

 

In this example MyStruct wraps a long.

public static class Program
{
    public static MyStruct X;

    public static void Main()
    {
        Program.X.A = 1234567887654321;
    }
}

Compiles to:

Program.Main:
    PUSH    EBP                                                 ; Function prologue.
    MOV     EBP, ESP                                            ; Function prologue.
    MOV     EAX, DWORD PTR DS:[3BD354C]                         ; Retrieve the address of the MyStruct object stored at the address where Program.X resides.
    MOV     DWORD PTR DS:[EAX + 4], 3C650DB1                    ; Store low DWORD of 1234567887654321 (The long begins at offset 4 since offset 0 is the object header pointer).
    MOV     DWORD PTR DS:[EAX + 8], 462D5                       ; Store high DWORD of 1234567887654321 (High DWORD of course is offset 4 more from the low DWORD).
    POP     EBP                                                 ; Function epilogue.
RET                                                             ; Return.

 

On a side note: These struct objects are allocated for all value type static variables for the class, the first time a method is called that accesses any static variable in the class.

Perhaps that's why they're doing it. To save memory. If you have a lot of structs in static classes, but you're not calling any methods on those classes that use them, you use less memory. If they were inlined in the static classes, then even if your program never accesses them, each struct would take up their size in memory for no reason. By allocating them on the heap as objects the first time they're accessed, you only take up their size in memory (+ pointer for the object header) when accessing them, and at most 8 bytes per variable when not accessing them. This also makes libraries smaller. But that's just speculation from my side as to why they might be doing it this way.

MulleDK19
  • 197
  • 1
  • 4
0

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.fixedaddressvaluetypeattribute?view=net-5.0

... plainly says, under "Remarks", that "Static value type fields are created as boxed objects. This means that their address can change as garbage collection is performed".