是一个在C#堆中装箱的静态值类型字段
本文关键字:静态 字段 类型 一个 | 更新日期: 2023-09-27 18:21:07
只是出于好奇-考虑以下示例:
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;
}
}
我的问题如下:是否因为Foo
字段驻留在堆上而将其值装箱?或者它是在一个特殊的容器对象/内存部分中封装的,就像实例值类型字段是堆上对象的一部分一样?
我认为它没有装箱,但我不确定,我找不到任何文件。
谢谢你的帮助。
CLR不具有类的每个字段都需要具有相同存储类型的限制。只有实例成员最终会出现在GC堆上。静态成员在加载器堆中分配。或者当字段具有[ThreadStatic]属性时,在线程本地存储中。这当然强制了一个约定,即静态成员由类的对象的每个实例共享。
非常简单地实现btw,抖动分配存储并知道字段的地址。因此,任何加载和存储都直接使用变量的地址。没有额外的指针取消引用,非常有效。
所以,不,根本不需要装箱,静态int将只占用4个字节。
如果您想亲自查看,请使用Debug+Windows+反汇编窗口。显示机器代码,您将直接使用变量的地址看到它。每次运行程序时,它都会是一个不同的地址,这是一种恶意软件计数器。
由于Sriram和Lee在问题评论中给出了答案,但没有给出答案,我将总结结果:
否,该值未装箱。值类型可以驻留在堆中,只有当它们像引用类型一样使用时才会被装箱
您还可以看到,在我的示例的IL代码中没有涉及拳击:
.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
TL;DR:是的,但不是语义上的,而且只适用于非内置值类型。
以下内容基于我自己对CLR应用程序内部工作的逆向工程
所提供的答案并不完全正确,事实上,相当具有误导性。
这是一个有趣的故事。这取决于情况。
内置类型(由VES直接支持),如int、float等,原始存储在静态变量的地址。
但有趣的是,像System.Decimal、System.DateTime和用户定义的值类型这样的非内置类型都被装箱了。
但有趣的是,他们实际上有点。。。双盒装。想象一下:
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);
}
}
现在,这将如您所期望的那样工作,MyStruct将被装箱用于PrintType,而不是装箱用于PrintA。
然而,Program.X实际上并不像在实例变量或局部变量中那样直接包含MyStruct实例。相反,它在堆中包含对它的引用,其中实例作为一个具有对象标头和all的对象存在。
如前所述,这不适用于内置类型。因此,如果您有一个包含int的静态变量,该静态变量将占用4个字节。但是,如果您有一个用户定义类型的静态变量,例如struct IntWrapper{public int A;}
,那么该静态变量在32位进程中将占用4个字节,在64位进程中占用8个字节来存储IntWrapper结构的盒装版本的地址,其中它在32位过程中占8个字节,忽略任何潜在的填充。
然而,从语义上讲,它的工作方式与您所期望的一样。当调用PrintA(Program.X)时,程序将复制Program.X指向的对象中的结构部分(对象标头后的数据),并将其传递给PrintA。
当调用PrintType(Program.X)时,它确实会装箱实例。该代码创建一个带有对象头的新MyStruct对象,然后将Program.X引用的对象中的a字段复制到新创建的对象中,然后将该对象传递给PrintType。
总之,Program.X包含装箱的MyStruct的地址(如果我们将装箱定义为将值类型转换为引用类型),但仍会将该对象装箱(或克隆),就像它是值类型一样,因此语义保持不变,就像它直接作为值类型存储在静态变量中一样。
就像我说的,我不确定他们为什么要这样做,但他们确实如此。
我已经包含了上面C#代码的JIT反汇编,并对其进行了评论。注意,我已经想出了反汇编中的所有名称。
关于调用的注释:所有对托管方法的调用都是通过指针进行的。在第一次调用时,指针指向负责JIT编译方法的代码。JIT编译后,指针将替换为JIT编译代码的地址,因此任何后续调用都很快。
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.
下面比较一下像long这样的内置类型和其他值类型之间的区别。
public static class Program
{
public static long X;
public static void Main()
{
Program.X = 1234567887654321;
}
}
编译为:
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.
在本例中,MyStruct包装一个长的。
public static class Program
{
public static MyStruct X;
public static void Main()
{
Program.X.A = 1234567887654321;
}
}
编译为:
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.
附带说明:这些结构对象是为类的所有值类型静态变量分配的,这是第一次调用访问类中任何静态变量的方法。
也许这就是他们这么做的原因。为了保存记忆。如果静态类中有很多structs,但没有在使用它们的类上调用任何方法,则会占用较少的内存。如果它们被内联在静态类中,那么即使程序从未访问过它们,每个结构也会毫无理由地占用它们在内存中的大小。通过在第一次访问它们时将它们作为对象分配到堆中,访问它们时只占用它们在内存中的大小(对象头的+指针),不访问它们时每个变量最多占用8字节。这也使库变得更小。但这只是我这边的猜测,他们为什么会这样做。
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.fixedaddressvaluetypeattribute?view=net-5.0
直截了当地说;备注";,";静态值类型字段被创建为装箱对象。这意味着它们的地址可以随着垃圾收集的执行而改变;。