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

时间:2010-07-29 11:28:32

标签: c# .net byte

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

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

8 个答案:

答案 0 :(得分:102)

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

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

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

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

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

答案 1 :(得分: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);
  }
}

答案 2 :(得分:9)

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

答案 3 :(得分:6)

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

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

无论如何,您可以下载BSD许可库(和CIL)from BitBucket。您还可以查看一些示例代码和更多详细信息at my blog

答案 4 :(得分:4)

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

int size = Unsafe.SizeOf<MyStruct>();

它也适用于参考类型(根据您的计算机架构将返回4或8)。

答案 5 :(得分:4)

<强> 0。对于示例代码:

using System;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

<强> 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;
};

<强> 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) { ... }
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));
    }
};

第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));
    }
};

<强> 4。结果与讨论

可以使用以下任意值将StructLayout(..., Pack)设置添加到struct T的声明中: {0,1,2,4,8,16,32,64,128 } 即可。未指定Pack时或与Pack=0等效的默认值 - 将包装设置为IntPtr.Size(x86上为4,x64上为8。< / p>

运行上述程序的结果显示Pack值仅影响Marshal.SizeOf报告的编组大小,而不影响单个{{1}的实际大小内存映像,假设是物理上相邻的实例之间的字节偏移量。测试代码通过分配给 rgt 的诊断托管数组T来衡量。

  

new T[2] ========= 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

如上所述,我们发现每个架构( x86 x64 ), 托管字段布局 无论&rgt[1] - &rgt[0]: 24设置如何,都是一致的。以下是32位和64位模式的实际管理字段偏移,如上面的代码所示:

  

Pack
   ┌─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

在此表中需要注意的最重要的事情是,(作为mentioned by Hans),报告的字段偏移对于其声明顺序而言是非单调的。 e short 2 12 16实例的字段始终重新排序,以便所有引用类型的字段首先出现。我们可以看到ValueType字段 d 位于偏移0处。

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

当然,通过对上一个表的行进行排序,我们可以揭示 .NET byte的真正内部托管布局。请注意,尽管示例struct ValueType包含托管引用(T),因此我们能够获得此布局,因此 非blittable

  

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

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

<强> 5。尾
为了完整起见,最后一个表格显示了在 编组 期间即时合成的填充量。请注意,在某些情况下,此T填充与内部管理规模相比为负。例如,即使 x64 中的Marshal的内部托管大小为24个字节,编组发出的结构也可以是19或20个字节,T或{{1分别。

  

Pack=1 Pack=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

答案 6 :(得分:3)

您想使用System.Runtime.InteropServices.Marshal.SizeOf()

struct s
{
    public Int64 i;
}

public static void Main()
{
    s s1;
    s1.i = 10;          
    var s = System.Runtime.InteropServices.Marshal.SizeOf(s1);
}

答案 7 :(得分:2)

您也可以使用System.Runtime.InteropServices.Marshal.SizeOf()来获取字节大小。