【发布时间】:2017-12-23 16:54:37
【问题描述】:
如果我正在创建一个比较大的结构,如何计算它在内存中占用的字节数?
我们可以手动完成,但是如果结构足够大,那我们怎么做呢?是否有一些代码块或应用程序?
【问题讨论】:
如果我正在创建一个比较大的结构,如何计算它在内存中占用的字节数?
我们可以手动完成,但是如果结构足够大,那我们怎么做呢?是否有一些代码块或应用程序?
【问题讨论】:
很长一段时间以来,结构体一直是计算机工程中的麻烦动物。它们的内存布局非常依赖于硬件。为了使它们高效,它们的成员必须对齐,以便 CPU 可以快速读取和写入它们的值,而不必多路复用字节以适应内存总线宽度。每个编译器都有自己的成员打包策略,通常由例如 C 或 C++ 程序中的#pragma pack 指令指示。
这没关系,但在互操作场景中是个问题。其中一块代码可能对结构布局做出与另一块不同的假设,由不同的编译器编译。您可以在 COM 中看到这一点,COM 是 .NET 的互操作编程的祖父解决方案。 COM 对处理结构的支持非常很差。它不支持将它们作为本机自动化类型,但通过 IRecordInfo 接口提供了一种解决方法。它允许程序通过在类型库中显式声明结构来发现运行时的内存布局。哪个工作正常,但效率很低。
.NET 设计者做出了一个非常勇敢且正确的决定来解决这个问题。他们使结构的内存布局完全无法发现。没有记录的方法来检索成员的偏移量。并且通过扩展,无法发现结构的大小。每个人都喜欢的答案,使用 Marshal.SizeOf() 实际上不是解决方案。这将返回结构 编组后的大小,即在调用 Marshal.StructureToPtr 之前需要传递给 Marshal.AllocCoTaskMem() 的大小。这会根据与结构关联的 [StructLayout] 属性来排列和对齐结构成员。请注意,结构不需要此属性(就像类一样),运行时实现了一个使用声明的成员顺序的默认属性。
布局不可发现的一个非常好的副作用是 CLR 可以使用它。在打包结构的成员并对齐它们时,布局可能会出现不存储任何数据的漏洞。称为填充字节。鉴于布局是不可发现的,CLR 实际上可以使用填充。如果它足够小以适合这样的孔,它会移动一个成员。现在,您实际上会得到一个结构,其大小小于,比给定声明的结构布局通常需要的大小。而且,值得注意的是,Marshal.SizeOf() 将返回结构大小的错误值,它返回的值太大。
长话短说,没有通用的方法可以通过编程方式获得结构大小的准确值。最好的办法就是不要问这个问题。 Marshal.SizeOf() 会给你一个猜测,假设结构是 blittable。如果您出于某种原因需要一个准确的值,那么您可以查看声明结构类型的局部变量的方法的生成机器代码,并将其与没有该局部变量的相同方法进行比较。您会看到堆栈指针调整的不同之处,即方法顶部的“sub esp, xxx”指令。当然,这取决于架构,您通常会在 64 位模式下获得更大的结构。
【讨论】:
您可以使用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);
}
}
【讨论】:
Marshal.SizeOf() 方法而不是sizeof() 关键字?编写sizeof(int) 不仅比Marshal.SizeOf(typeof(T)) 更有效,因为它不会产生运行时成本,而且更易于阅读。 Marshal.SizeOf() 只能用于在非托管场景中使用的类型(例如 P/Invoke)。
sizeof 从 .NET 2.0 开始就很好,因为它不再需要 unsafe。
sizeof 运算符仍然需要 unsafe 上下文。从 .NET 2.0 开始的例外是允许在 CTS 类型上使用它。
您可以将sizeof() 关键字用于不包含作为引用类型的任何字段或属性的用户定义结构,或者使用Marshal.SizeOf(Type) 或Marshal.SizeOf(object) 来获取类型的非托管大小或具有顺序或显式 layout 的结构。
【讨论】:
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)。
运行上述程序的结果表明Pack的值只影响Marshal.SizeOf报告的marshaling size,而不影响单个T内存映像的实际大小,假设是物理相邻实例之间的字节偏移量。测试代码通过分配给 rgt 的诊断托管数组 new T[2] 来衡量这一点。
========= x86 =================== x64 ==========
-------- Pack=1 ---------------- Pack=1 --------Marshal.Sizeof(): 15Marshal.Sizeof(): 19&rgt[1] - &rgt[0]: 16&rgt[1] - &rgt[0]: 24
-------- Pack=2 ---------------- Pack=2 --------Marshal.Sizeof(): 16Marshal.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]: 16987654357 >&rgt[1] - &rgt[0]: 16&rgt[1] - &rgt[0]: 24
-- Pack=16/32/64/128 ----- Pack=16/32/64/128 ---Marshal.Sizeof(): 20Marshal.Sizeof(): 32&rgt[1] - &rgt[0]: 16&rgt[1] - &rgt[0]: 24
如上所述,我们发现每个架构(x86、x64)的托管字段布局无论Pack 设置如何,都是一致的。下面是实际的托管字段偏移量,同样适用于 32 位和 64 位模式,如上面的代码所示:
┌─offs─┐field type size x86 x64===== ====== ==== === ===a int 4 4 8b byte 1 14 18c int 4 8 12d String 4/8 0 0e short 2 12 16
在此表中要注意的最重要的一点是,(如mentioned by Hans),报告的字段偏移量就其声明顺序而言是非单调的。 ValueType 实例的字段总是重新排序,以便所有引用类型的字段都排在第一位。我们可以看到String 字段 d 位于偏移量 0 处。
进一步的重新排序优化了字段顺序,以便共享内部多余的填充,否则会被浪费。我们可以通过 byte 字段 b 看到这一点,该字段已从第二个声明的字段移至最后一个。
当然,通过对上表的行进行排序,我们可以揭示 .NET ValueType 的真正内部托管布局。请注意,尽管示例结构 T 包含托管引用 (String d) 并且因此是不可blittable,我们仍然能够获得此布局:
============= x86 ============@============= x64 ============field type size offs end@field type size offs end===== ====== ==== ==== ===987654390@987654390 @c int 4 12 … 16e short 2 12 … 14e short 2 16 … 18b byte 1 14 … 15b byte 1 18 … 19
internal padding: 1 15 … 16internal padding: 5 19 … 24
x86 managed total size: 16x64 managed total size: 24
之前我们通过计算相邻实例之间的字节偏移量差异来确定单个托管结构实例的大小。考虑到这一点,上表的最后几行简单地显示了 CLR 内部应用到示例结构 T 末尾的填充。当然,请再次记住,这个内部填充是由 CLR 修复的,完全超出我们的控制范围。
5.尾声
为完整起见,最后一个表格显示了在 编组 期间将on-the-fly 合成的填充量。请注意,在某些情况下,此 Marshal 填充与内部托管大小相比为负数。例如,即使 x64 中 T 的内部管理大小为 24 字节,编组发出的结构也可以分别为 19 或 20 字节,Pack=1 或 Pack=2。
pack size offs end@pack size offs end============= ==== ==== ================ ==== ==== ===1 0 15 … 15987654414@9876544415 p>
【讨论】:
我用CIL(.NET 的汇编语言)编写了一个很小的库,以公开一些 C# 中没有的简洁功能。我打破了sizeof 指令。
它与 C# 中的 sizeof 运算符有很大不同。基本上,它获取结构(或引用类型,通过一些优化表现得有趣)的大小,包括填充和所有。因此,如果您要创建一个T 的数组,那么您可以使用 sizeof 来确定每个数组元素之间的距离以字节为单位。它也是完全可验证和托管的代码。请注意,Mono 中有一个错误(3.0 之前的版本?)会导致引用类型的 sizeof 报告不正确,这将扩展到包含引用类型的结构。
无论如何,您可以下载 BSD 许可库(和 CIL)from BitBucket。您还可以查看一些示例代码和更多详细信息at my blog。
【讨论】:
在.NET Core 中,sizeof CIL 指令已通过最近添加的Unsafe 类公开。添加对System.Runtime.CompilerServices.Unsafe 包的引用,然后执行以下操作:
int size = Unsafe.SizeOf<MyStruct>();
它也适用于引用类型(将返回 4 或 8,具体取决于您的计算机架构)。
【讨论】:
你想使用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);
}
【讨论】:
您也可以使用System.Runtime.InteropServices.Marshal.SizeOf() 来获取以字节为单位的大小。
【讨论】: