这是一个常见面试题,值类型(Value Type)和引用类型(Reference Type)有什么区别?他们性能方面有什么区别?
TL;DR(先看结论) 值类型 引用类型创建位置 栈 托管堆
赋值时 复制值 复制引用
动态内存分配 无 需要分配内存
额外内存消耗 无 32位:额外12字节;64位:24字节
内存分布 连续 分散
引用类型 常用的引用类型代码示例: void Main() { // 开始计数器 var sw = Stopwatch.StartNew(); long memory1 = GC.GetAllocatedBytesForCurrentThread(); // 创建C16 Span<B16> data = new B16[40_0000]; foreach (ref B16 item in data) { item = new B16(); item.V15.V15.V0 = 1; } long sum = 0; // 求和以免代码被优化掉 for (var i = 0; i < data.Length; ++i) { sum += data[i].V15.V15.V0; } // 终止计数器 sw.Stop(); long memory2 = GC.GetAllocatedBytesForCurrentThread(); // 输出显示结果 new { Sum = sum, CreateTime = sw.ElapsedMilliseconds, Memory = memory2 - memory1 }.Dump(); } class A1 { public byte V0; } class A16 { public A1 V0, V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15; public A16() { V0 = new A1(); V1 = new A1(); V2 = new A1(); V3 = new A1(); V4 = new A1(); V5 = new A1(); V6 = new A1(); V7 = new A1(); V8 = new A1(); V9 = new A1(); V10 = new A1(); V11 = new A1(); V12 = new A1(); V13 = new A1(); V14 = new A1(); V15 = new A1(); } } class B16 { public A16 V0, V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15; public B16() { V0 = new A16(); V1 = new A16(); V2 = new A16(); V3 = new A16(); V4 = new A16(); V5 = new A16(); V6 = new A16(); V7 = new A16(); V8 = new A16(); V9 = new A16(); V10 = new A16(); V11 = new A16(); V12 = new A16(); V13 = new A16(); V14 = new A16(); V15 = new A16(); } }
这次代码中,我们创建了40万个B16类型,然后对这40万个B16进行了统计,其中:
A1是一个字节(byte)的class;
A16是包含16个A1的class;
B16是包含16个A16的class;
可以计算出,B16=16·A16=16x16·A1=16x16x256 bytes,一共分配了40万个B16,所以一共有40_0000x256=1_0240_0000 bytes,或约100兆字节。
实际结果输出 Sum CreateTime Memory40_0000 8_681 3_440_000_304
电脑配置(之后的下文的性能测试结果与此完全相同):
项目/配置 配置 说明CPU E3-1230 v3 @ 3.30GHz 未超频
内存 24GB DDR3 1600 MHz 8GB x 3
.NET Core 3.0.100-preview7-012821 64位
软件 LINQPad 6.0.13 64位,optimize+
数字涵义:
40万条数据对1求和,结果是40万,正确;
总花费时间一共需要9417毫秒;
总内存开销约为3.4GB。
请注意看内存开销,我们预估值是100MB,但实际约为3.4GB,这说明了引用类型需要(较大的)额外内存开销。
一个空对象 要分配多大的堆内存?以一个空白引用类型为例,可以写出如下代码(LINQPad中运行):
long m1 = GC.GetAllocatedBytesForCurrentThread(); var obj = new object(); long m2 = GC.GetAllocatedBytesForCurrentThread(); (m2 - m1).Dump(); GC.KeepAlive(obj);注意GC.KeepAlive是有必要的,否则运行在optimize+环境下会将new object()优化掉。
运行结果:24(在32位系统中,运行结果为:12)
空引用类型(64位)为何要24个字节?一个引用类型的堆内存包含以下几个部分:
同步块索引(synchronization block index),8个字节,用于保存大量与CLR相关的元数据,以下基本操作都会用到该内存:
线程同步(lock)
垃圾回收(GC)
哈希值(HashCode)
其它
方法表指针(method table pointer),又叫类型对象指针(TypeHandle),8个字节,用来指向类的方法表;
实例成员,8字节对齐,没有任何成员时也需要8个字节。
由于以上几点,才导致一个空白的object需要24个字节。
因为没有同步块索引,导致:
值类型不能参与线程同步(lock)
值类型不需要进行垃圾回收(GC)
值类型的哈希值计算过程与引用类型不同(HashCode)
因为没有方法表指针,导致:
值类型不能继承
值类型的性能 值类型代码示例 void Main() { // 开始计数器 var sw = Stopwatch.StartNew(); long memory1 = GC.GetAllocatedBytesForCurrentThread(); // 创建C16 Span<B16> data = new B16[40_0000]; foreach (ref B16 item in data) { // item = new B16(); item.V15.V15.V0 = 1; } long sum = 0; // 求和以免代码被优化掉 for (var i = 0; i < data.Length; ++i) { sum += data[i].V15.V15.V0; } // 终止计数器 sw.Stop(); long memory2 = GC.GetAllocatedBytesForCurrentThread(); // 输出显示结果 new { Sum = sum, CreateTime = sw.ElapsedMilliseconds, Memory = memory2 - memory1 }.Dump(); } struct A1 { public byte V0; } struct A16 { public A1 V0, V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15; } struct B16 { public A16 V0, V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15; }几乎完全一样的代码,区别只有:
将所有的class(表示引用类型)关键字换成了struct(表示值类型)
将item = new B16()语句去掉了(因为值类型创建数组会自动调用默认构造函数)
运行结果运行结果如下:
Sum CreateTime Memory40_0000 32 102_400_024