如何在C#中测量UTF8编码字符串相对UTF16的内存占用收益
验证
ReadOnlySpan<byte>与string的内存占用差异 首先,你遇到的Marshal.SizeOf()报错翻译为:
类型‘System.String’无法作为非托管结构进行封送处理;无法计算有意义的大小或偏移量。
这个报错的核心原因是:Marshal.SizeOf()仅适用于非托管类型或可直接封送的托管类型,而.NET中的string是带有CLR头部(同步块索引、类型指针、长度等元数据)的托管对象,并非简单的字节序列,因此无法用该工具获取实际内存占用。
下面是几种直观验证两者内存差异的方法:
1. 直接计算编码字节数+对象开销
string utf16 = "ab→cd":.NET字符串默认采用UTF-16编码,每个字符占2字节。这段字符串包含5个Unicode字符(a、b、→、c、d),内容本身占5×2=10字节。加上64位系统下string的固定头部开销(24字节:8字节同步块索引+8字节类型指针+4字节长度+4字节预留),总内存占用为34字节,CLR会按8字节对齐规则分配,实际占用40字节左右。ReadOnlySpan<byte> utf8 = "ab→cd"u8:UTF-8编码下,a/b/c/d各占1字节,→(U+2192)占3字节,内容总字节数为1+1+3+1+1=7字节。而ReadOnlySpan<byte>是值类型,在栈上仅占16字节(64位系统:8字节指针+8字节长度),它指向编译时嵌入程序的静态只读UTF-8字节数组(该数组内存仅占用7字节,对齐后为8字节)。由于Span本身不持有内存,仅作为引用,整体内存开销远小于string。
2. 通过GC.GetTotalMemory观察内存增量
通过创建大量实例对比内存变化,能直观看到差异:
// 先执行Full GC,获取初始内存基准 GC.Collect(); GC.WaitForPendingFinalizers(); long initialMemory = GC.GetTotalMemory(true); const int instanceCount = 1000000; // 测试string内存占用(避免字符串驻留,用动态生成的字符串) var stringArray = new string[instanceCount]; for (int i = 0; i < instanceCount; i++) { stringArray[i] = "ab→cd" + i.ToString(); } long stringMemory = GC.GetTotalMemory(true) - initialMemory; Console.WriteLine($"100万个string实例的内存增量:{stringMemory:N0} 字节"); // 清理string并重新GC Array.Clear(stringArray); GC.Collect(); GC.WaitForPendingFinalizers(); initialMemory = GC.GetTotalMemory(true); // 测试ReadOnlySpan<byte>内存占用 var spanArray = new ReadOnlySpan<byte>[instanceCount]; for (int i = 0; i < instanceCount; i++) { spanArray[i] = "ab→cd"u8; } long spanMemory = GC.GetTotalMemory(true) - initialMemory; Console.WriteLine($"100万个ReadOnlySpan实例的内存增量:{spanMemory:N0} 字节");
运行后会发现,string的内存增量是Span的数十倍——因为每个string都是独立的托管对象,而Span数组里仅存指向同一段静态数组的引用,几乎没有额外堆内存开销。
3. 用调试工具查看内存布局
在Visual Studio中开启「内存」窗口:
- 定位到
string对象地址,前24字节是CLR头部,后续是UTF-16编码的字节序列:61 00 62 00 92 21 63 00 64 00(对应a、b、→、c、d的UTF-16小端编码)。 - 找到
"ab→cd"u8对应的静态字节数组,内存内容为61 62 E2 86 92 63 64(UTF-8编码),而ReadOnlySpan<byte>的内存仅为指向该数组的指针+长度值,占16字节栈空间。
内容的提问来源于stack exchange,提问作者FluidMechanics Potential Flows
相关产品推荐
相关产品推荐

