如何检查结构消耗的字节数?

Sim*_*ons 42 .net c# byte

如果我创建一个相对较大的结构,我如何计算它在内存中占用的字节数?

我们可以手动完成,但如果结构足够大,那么我们该怎么做呢?是否有一些代码块或应用程序?

Han*_*ant 107

很长一段时间,结构在计算机工程中一直是麻烦的野兽.它们的内存布局与硬件有关.为了使它们有效,它们的成员必须对齐,以便CPU可以快速读取和写入它们的值,而不必复用字节以适应内存总线宽度.每个编译器都有自己的成员打包策略,通常由例如C或C++程序中的#pragma pack指令指导.

这是可以的,但在互操作方案中是一个问题.其中一个代码块可能对结构布局做出不同于另一个块的假设,由另一个编译器编译.你可以在COM,.NET的祖父解决方案中看到这个互操作编程.COM具有非常的处理结构的支持比较差.它不支持它们作为本机自动化类型,但通过IRecordInfo接口有一个解决方法.这允许程序通过在类型库中显式声明结构来在运行时发现内存布局.哪个工作正常,但效率很低.

.NET设计者做出了非常勇敢,正确的决定来解决这个问题.他们使结构的内存布局完全无法发现.没有记录的方法来检索成员的偏移量.而通过扩展,无法发现结构的大小.每个人最喜欢的答案,使用Marshal.SizeOf()实际上不是解决方案.在编组后,返回struct 的大小,在调用Marshal.StructureToPtr之前,需要传递给Marshal.AllocCoTaskMem()的大小.根据与struct关联的[StructLayout]属性排列和对齐struct成员.请注意,结构不需要此属性(类似于类),运行时实现默认的属性,该属性使用成员的声明顺序.

布局无法发现的一个非常好的副作用是CLR可以用它来玩弄技巧.打包结构的成员并对齐它们时,布局可能会出现不存储任何数据的漏洞.称为填充字节.鉴于布局是不可发现的,CLR实际上可以使用填充.如果它足够小以适合这样的洞,它会移动一个成员.现在,您将获得一个结构,其大小小于给定声明的结构布局时通常需要的结构.而且,值得注意的是,Marshal.SizeOf()将返回错误的结构大小值,它返回的值太大.

长话短说,没有通用的方法来以编程方式获得结构大小的准确值.最好的办法就是不要问这个问题.假设结构是blittable,Marshal.SizeOf()将给你一个估计.如果由于某种原因需要准确的值,那么您可以查看生成的结构类型的局部变量的方法的机器代码,并将其与没有该局部变量的相同方法进行比较.您将看到堆栈指针调整的差异,方法顶部的"sub esp,xxx"指令.当然,它将依赖于体系结构,您通常会在64位模式下获得更大的结构.

  • @Hans:结构化的C++ DLL编组怎么样?我不认为这在使用LayoutKind.Sequential时适用.每个MSDN:"表示该类型的成员将按照它们在托管类型定义中出现的顺序在非托管内存中进行布局."因此CLR根本不应该填充,否则这些位将是混乱的. (3认同)

Shi*_*mmy 30

您可以使用sizeof运算符或SizeOf函数.
这些选项之间存在一些差异,请参阅参考链接以获取更多信息.

无论如何,使用该函数的一个好方法是使用这样的泛型方法或扩展方法:

static class Test
{
  static void Main()
  {
    //This will return the memory usage size for type Int32:
    int size = SizeOf<Int32>();

    //This will return the memory usage size of the variable 'size':
    //Both lines are basically equal, the first one makes use of ex. methods
    size = size.GetSize();
    size = GetSize(size);
  }

  public static int SizeOf<T>()
  {
    return System.Runtime.InteropServices.Marshal.SizeOf(typeof(T));
  }

  public static int GetSize(this object obj)
  {
    return System.Runtime.InteropServices.Marshal.SizeOf(obj);
  }
}
Run Code Online (Sandbox Code Playgroud)

  • `sizeof`运算符仍然需要`unsafe`上下文.从.NET 2.0开始的例外是允许它在CTS类型上使用. (5认同)
  • +1但要注意这会返回非托管大小,这可能会或可能不会等于托管大小.但是听起来对OP来说并不重要.此外,`sizeof`从.NET 2.0开始就很好,因为它不再需要`不安全'. (3认同)
  • 您为什么认为应该使用Marshal.SizeOf()方法而不是关键字sizeof()?编写sizeof(int)不仅比Marshal.SizeOf(typeof(T))更高效,因为它不会增加运行时成本,而且更易于阅读。Marshal.SizeOf()应该仅用于打算在非托管场景中使用的类型(例如P / Invoke)。 (2认同)

All*_*nek 9

您可以将sizeof()关键字用于不包含任何字段或属性作为引用类型的用户定义结构,也可以使用Marshal.SizeOf(Type)Marshal.SizeOf(object)获取具有顺序或显式布局的类型或结构的非托管大小.


Ear*_*rlz 7

我用CIL(.NET的汇编语言)编写了一个小小的库,以展示C#中没有的一些简洁的功能.我打破了sizeof指示.

它与sizeofC#中的运算符有很大不同.基本上,它获得了结构(或引用类型,通过一些优化表现得很有趣)的大小,包括填充和所有.因此,如果要创建一个数组T,则可以使用sizeof来确定每个数组元素之间的距离(以字节为单位).它也是完全可验证的托管代码.请注意,在Mono中有一个错误(3.0之前的版本?)会导致sizeof引用类型被错误地报告,这会扩展到包含引用类型的结构.

无论如何,您可以从BitBucket下载BSD许可库(和CIL).您还可以在我的博客上查看一些示例代码和更多详细信息.


Jam*_* Ko 5

.NET Core 中sizeofCIL 指令已通过最近添加的Unsafe类公开。添加对System.Runtime.CompilerServices.Unsafe包的引用,然后执行以下操作:

int size = Unsafe.SizeOf<MyStruct>();
Run Code Online (Sandbox Code Playgroud)

它也适用于引用类型(将返回 4 或 8,具体取决于您的计算机架构)。


Gle*_*den 5

0.对于示例代码:

using System;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
Run Code Online (Sandbox Code Playgroud)

1.示范结构

[Serializable, StructLayout(LayoutKind.Sequential, Pack = 128)]
public struct T
{
    public int a;
    public byte b;
    public int c;
    public String d;
    public short e;
};
Run Code Online (Sandbox Code Playgroud)

2.减去托管指针:

/// Return byte-offset between managed addresses of struct instances 'hi' and 'lo'
public static long IL<T1,T2>.RefOffs(ref T1 hi, ref T2 lo) { ... }
Run Code Online (Sandbox Code Playgroud)
public static class IL<T1, T2>
{
    public delegate long _ref_offs(ref T1 hi, ref T2 lo);

    public static readonly _ref_offs RefOffs;

    static IL()
    {
        var dm = new DynamicMethod(
            Guid.NewGuid().ToString(),
            typeof(long),
            new[] { typeof(T1).MakeByRefType(), typeof(T2).MakeByRefType() },
            typeof(Object),
            true);

        var il = dm.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Sub);
        il.Emit(OpCodes.Conv_I8);
        il.Emit(OpCodes.Ret);
        RefOffs = (_ref_offs)dm.CreateDelegate(typeof(_ref_offs));
    }
};
Run Code Online (Sandbox Code Playgroud)

3.显示托管的内部结构布局:

static class demonstration
{
    /// Helper thunk that enables automatic type-inference from argument types
    static long RefOffs<T1,T2>(ref T1 hi, ref T2 lo) => IL<T1,T2>.RefOffs(ref hi, ref lo);

    public static void Test()
    {
        var t = default(T);
        var rgt = new T[2];

        Debug.Print("Marshal.Sizeof<T>: {0,2}", Marshal.SizeOf<T>());
        Debug.Print("&rgt[1] - &rgt[0]: {0,2}", RefOffs(ref rgt[1], ref rgt[0]));

        Debug.Print("int      &t.a      {0,2}", RefOffs(ref t.a, ref t));
        Debug.Print("byte     &t.b      {0,2}", RefOffs(ref t.b, ref t));
        Debug.Print("int      &t.c      {0,2}", RefOffs(ref t.c, ref t));
        Debug.Print("String   &t.d      {0,2}", RefOffs(ref t.d, ref t));
        Debug.Print("short    &t.e      {0,2}", RefOffs(ref t.e, ref t));
    }
};
Run Code Online (Sandbox Code Playgroud)

4.结果与讨论

StructLayout(..., Pack)可以使用struct T以下任何值将设置添加到声明中:{0,1,2,4,4,8,16,32,64,128}Pack未指定默认值when (或与Pack=0它等效)将包装设置为等于IntPtr.Size4在x86上,8在x64上)。

运行上述程序的结果表明,该Pack值仅影响所报告的封送处理大小Marshal.SizeOf,而不影响单个T内存映像的实际大小(假定为物理上相邻实例之间的字节偏移量)。测试代码通过new T[2]分配给rgt的诊断托管数组对此进行测量。

========= x86 ==========     ========= x64 ==========

-------- Pack=1 --------     -------- Pack=1 --------
Marshal.Sizeof(): 15     Marshal.Sizeof(): 19
&rgt[1] - &rgt[0]: 16     &rgt[1] - &rgt[0]: 24

-------- Pack=2 --------     -------- Pack=2 --------
Marshal.Sizeof(): 16     Marshal.Sizeof(): 20
&rgt[1] - &rgt[0]: 16     &rgt[1] - &rgt[0]: 24

--- Pack=4/0/default ---     -------- Pack=4 --------
Marshal.Sizeof(): 20     Marshal.Sizeof(): 24
&rgt[1] - &rgt[0]: 16     &rgt[1] - &rgt[0]: 24

-------- Pack=8 --------     --- Pack=8/0/default ---
Marshal.Sizeof(): 20     Marshal.Sizeof(): 32
&rgt[1] - &rgt[0]: 16     &rgt[1] - &rgt[0]: 24

-- Pack=16/32/64/128 ---     -- Pack=16/32/64/128 ---
Marshal.Sizeof(): 20     Marshal.Sizeof(): 32
&rgt[1] - &rgt[0]: 16     &rgt[1] - &rgt[0]: 24

如前所述,我们发现对于每个体系结构(x86x64),无论设置如何,托管字段的布局都是一致的Pack。同样是32​​位和64位模式的实际托管字段偏移,如上面的代码所报告:

??offs??
field type size x86 x64
===== ====== ==== === ===
a int 4 4 8
b byte 1 14 18
c int 4 8 12
d String 4/8 0 0
e short 2 12 16

该表中最要注意的是(如Hans提到的),报告的字段偏移量相对于它们的声明顺序是非单调的。ValueType实例的字段始终会重新排序,以便所有引用类型的字段都排在最前面。我们可以看到String字段d在偏移量0处。

进一步的重新排序优化了场顺序,以便共享内部填充过多,否则将浪费这些内部填充。我们可以通过byte字段b看到这一点,该字段已从第二个声明的字段移动到最后一个声明的字段。

自然地,通过对上一个表的行进行排序,我们可以揭示.NET 的真正内部托管布局ValueType。请注意,尽管示例结构T包含托管引用(String d),因此我们无法获得该布局,但该结构不可变

============= x86 ============     ============= x64 ============
field type size offs end     field type size offs end
===== ====== ==== ==== ===     ===== ====== ==== ==== ===
d String 4 0 … 4     d String 8 0 … 8
a int 4 4 … 8     a int 4 8 … 12
c int 4 8 … 12     c int 4 12 … 16
e short 2 12 … 14     e short 2 16 … 18
b byte 1 14 … 15     b byte 1 18 … 19

internal padding: 1 15 … 16     internal padding: 5 19 … 24

x86 managed total size: 16            x64 managed total size: 24

前面我们通过计算相邻实例之间的字节偏移量差来确定单个托管结构实例的大小。考虑到这一点,上表的最后几行简单地显示了CLR内部应用于示例struct末尾的填充T。当然,请再次记住,内部填充是由CLR固定的,完全超出了我们的控制范围。

5.科达
为了完整性,这最后的表所示,将被合成的填充量对即时期间编组。请注意,在某些情况下,此Marshal填充相对于内部托管大小为负数。例如,即使一个内部管理的大小T64位是24个字节,通过编组发射的结构可以是19或20个字节,Pack=1Pack=2分别。

pack size offs end     pack size offs end
============= ==== ==== ===     ============= ==== ==== ===
1 0 15 … 15     1 0 19 … 19
2 1 15 … 16     2 1 19 … 20
4/8/16/32/64… 5 15 … 20     4/8/16/32/64… 5 19 … 24