.NET中的值类型与引用类型

这是一个常见面试题,值类型(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 Memory
40_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 Memory
40_0000 32 102_400_024

注意,分配内存只有102_400_024字节,比我们预估的102_400_000只多了24个字节。这是因为数组也是引用类型,引用类型需要至少24个字节。

比较

运行时间 时间比 分配内存 内存比
值类型 32 / 102_400_024 /
引用类型 8_681 271.28x 3_440_000_304 33.59x

在这个示例中,仅将值类型改成引用类型,竟需要多出271倍的时间,和33倍的内存占用。

重新审视值类型

值类型这么好,为什么不全改用值类型呢?

值类型的优点,恰恰也是值类型的缺点,值类型赋值时是复制值,而不是复制引用,而当值比较大时,复制值非常昂贵

在远古时代,甚至是没有动态内存分配的,所以世界上只有值类型。那时为了减少值类型复制,会用变量来保存对象的内存位置,可以说是最早的指针了。

在近代的的C里,除了值类型,还加入了指向动态分配的值类型的指针。其中指针基本可以与引用类型进行类比:

  • ✔指针和引用类型的引用,都指向真实的对象内存位置

  • ❌动态分配的内存需要手动删除,引用类型会自动GC回收

  • ❌指针指向的内存位置不会变,引用类型指向的内存位置会随着GC的内存压缩而产生变化,可用fixed关键字临时禁止内存压缩

  • ❌指针指向的内存没有额外消耗,引用类型需要分配至少24字节的堆内存

C++为了解决这个问题,也是卯足了劲。先是加入了值引用运算符 &,而后又发布了一版又一版的“智能”指针,如auto_ptr/shared_ptr/unique_ptr。但这些“智能”指针都需要提前了解它的使用场景,如:

  • 有对象所有权还是没有对象所有权?

  • 线程安全还是不安全?

  • 能否用于赋值?

而且库与库之前的版本多样,不统一,还影响开发的心情。

所以引用类型的优势就出来了,不用关心对象的所有权,不用关心线程安全,不用关心赋值问题,而且最重要的,还不用关心值类型复制的性能问题。

C#中的值类型支持

引用类型是如此好,以至于平时完全不需要创建值类型,就能完成任务了。但为什么值类型仍然还是这么重要呢?就是因为一旦涉及底层,性能关键型的服务器、游戏引擎等等,都需要关心内存分配,都需要使用值类型。

因为只有C#才能不依赖于C/C++等“本机语言”,就可写出性能关键型应用程序。

C#因为有这些和值类型的特性,导致与其它语言(C/C++)相比时完全不虚:

  • 首先,C#可以写自定义值类型

  • C# 7.0 值类型Task(ValueTask):大量异步请求,如读取流时,可以节省堆内存分配和GC
    链接:https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/

  • C# 7.0 ref返回值/本地变量引用:避免了大值类型内存大量复制的开销(有点像C++&关键字了)
    链接:https://devblogs.microsoft.com/dotnet/whats-new-in-csharp-7-0/#user-content-ref-returns-and-locals

  • C# 7.0 Span<T>Memory<T>,简化了ref引用的代码,甚至让foreach循环都可以操作修改值类型了
    链接:https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

  • C# 7.2 加入in修饰符和其它修饰符,相当于C++中的const TypeName&

    链接:https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-7-2#safe-efficient-code-enhancements

  • C# 8.0 - Preview 5 可Dispose的ref struct,值类型也能使用Dispose模式了
    链接:https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#disposable-ref-structs

ASP.NET Core曾使用Libuv(基于C语言)作为内部传输层,但从ASP.NET Core 2.1之后,换成了用.NET重写,链接:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-2.2#transport-configuration

最后的话

开发经常拿C#与同样开发Web应用的其它语言作比较,但由于缺乏对值类型的支持,这些语言没办法与C#相比。

其中Java还暂不支持自定义值类型。

推荐书籍:《C#从现象到本质》(郝亦非 著)


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

.NET中的值类型与引用类型相关推荐

  1. 述说C#中的值类型和引用类型的千丝万缕

    关于值类型和引用类型方面的博客和文章可以说是汗牛充栋了,今天无意中又复读了一下这方面的知识,感觉还是有许多新感悟的,就此时间分享一下: CLR支持两种类型:值类型和引用类型,看起来FCL的大多数类型是 ...

  2. C#中的值类型和引用类型

    文章目录 1 C#中的值类型 1.1 值类型示例程序 1.2 值类型(基本数据类型)的变量使用特点 2 C#中的引用类型 2.1 引用类型数据程序示例 2.2 引用数据类型的变量使用特点 3 变量类型 ...

  3. Windows Phone 开发起步之旅之二 C#中的值类型和引用类型

    今天和大家分享下本人也说不清楚的一个C#基础知识,我说不清楚,所以我才想把它总结一下,以帮助我自己理解这个知识上的盲点,顺便也和同我一样不是很清楚的人一起学习下.  一说起来C#中的数据类型有哪些,大 ...

  4. .net知识和学习方法系列(十七)CLR-CLR中的值类型和引用类型

    C#中有谈到两种类型,值类型和引用类型(其实是CLR支持两种类型). 值类型包括:简单类型(int ,double,long,bool,char等,string除外),struct,enum 引用类型 ...

  5. golang中的值类型和引用类型

    值类型与引用类型 不管是Java还是golang中,都有值类型和引用类型的概念.在使用两者时,发现这两种语言之间还是有差异的. 值类型 值类型:这些类型的变量直接指向存在内存中的值,值类型的变量的值存 ...

  6. Java 基础 —— Java 中的值类型与引用类型

    一.值类型与引用类型 在 Java 中类型可分为两大类:值类型与引用类型.值类型就是基本数据类型(如 int.double 等),而引用类型是指除了基本的变量类型之外的所有类型(如通过 class 定 ...

  7. python中的引用类型_Python中的值类型与引用类型

    其实各个标准资料中没有说明Python有值类型和引用类型的分类,这个分类一般是C++和Java中的.但是语言是相通的,所以Python肯定也有类似的.实际上Python 的变量是没有类型的,这与以往看 ...

  8. C#中的值类型(value type)与引用类型(reference type)的区别

    ylbtech- .NET-Basic:C#中的值类型与引用类型的区别 C#中的值类型(value type)与引用类型(reference type)的区别 1.A,相关概念返回顶部 C#中有两种数 ...

  9. string:值类型?引用类型?[转]

    string是一种很特殊的数据类型,它既是基元类型又是引用类型,在编译以及运行时,.Net都对它做了一些优化工作,正式这些优化工作有时会迷惑编程人员,使string看起来难以琢磨,这篇文章分上下两章, ...

最新文章

  1. R语言将dataframe长表转化为宽表实战:使用reshape函数、使用tidyr包的spread函数、使用data.table
  2. 皮一皮:论蓝朋友的拍摄技术
  3. ansible(1)——安装
  4. 一个奇怪的注意事项TNS-12545 TNS-12560 TNS-00515
  5. JSON为什么那样红(另有洞天)
  6. 影响力-你为什么说是
  7. javascript提醒
  8. Memcached windows 下安装与应用
  9. 几校联考——day1题解
  10. ssh整合(spring + struts2 + hibernate)xml版
  11. ppt格式刷快捷键_15个PPT神操作,让老师做课件的效率翻倍!
  12. MATLAB | 好看的相关系数矩阵图绘制
  13. Navicat: Cannot create filec:\Users\***\Documens\Navicat\MySql.....文件名、目录名或卷标语法不正确
  14. 如何设计出骚气的秒杀系统?
  15. TP6 workman安装踩坑
  16. gom及gee小白架设黑屏的原因以及个别装备地图不显示怎么办?
  17. 冰河的大学生活,两个好基友:二神和波妞,哈哈,挺有意思的
  18. 弹性盒子布局(下面有代码)
  19. 用HTML5实现十里桃花歌词的打印(一)
  20. 客服回复话术100句

热门文章

  1. windows os x_如何立即在OS X上获取Windows样式的窗口捕捉
  2. 计算机网络拓跋结构,实战 | 服务端开发与计算机网络结合的完美案例
  3. html仿微信滑动删除,使用Vue实现移动端左滑删除效果附源码
  4. 【No.7 C++对象的构造与析构时间】
  5. hdoj-1005-Number Sequences
  6. [LeetCode]119.Pascal#39;s Triangle II
  7. Sql plus命令报command not found的解决笔记
  8. python类库32[多线程同步Lock+RLock+Semaphore+Event]
  9. 65 + iPhone应用程序网站创意设计灵感(上篇)
  10. 【谷歌】Google Chrome 浏览器中 font-size 12px 没有效果