.NET中的值类型与引用类型
.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 点击查看C# 7.0ref返回值/本地变量引用:避免了大值类型内存大量复制的开销(有点像C++的&关键字了) 点击查看C# 7.0Span<T>和Memory<T>,简化了ref引用的代码,甚至让foreach循环都可以操作修改值类型了 点击查看C# 7.2加入in修饰符和其它修饰符,相当于C++中的const TypeName&点击查看C# 8.0 - Preview 5可Dispose的ref struct,值类型也能使用Dispose模式了 点击查看
ASP.NET Core曾使用Libuv(基于C语言)作为内部传输层,但从ASP.NET Core 2.1之后,换成了用.NET重写。
最后的话
开发经常拿C#与同样开发Web应用的其它语言作比较,但由于缺乏对值类型的支持,这些语言没办法与C#相比。
其中Java还暂不支持自定义值类型。
推荐书籍:《C#从现象到本质》(郝亦非 著)
作者:周杰
出处:https://www.cnblogs.com/sdflysha
本文采用
知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议
进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
.NET中的值类型与引用类型的更多相关文章
- C# 中的值类型和引用类型
原文 C# 中的值类型和引用类型 值类型(value type):int,long,float,double,decimal,char,bool 和 struct 统称为值类型.值类型变量声明后,不管 ...
- C++ : 从栈和堆来理解C#中的值类型和引用类型
C++中并没有值类型和引用类型之说,标准变量或者自定义对象的存取默认是没有区别的.但如果深入地来看,就要了解C++中,管理数据的两大内存区域:栈和堆. 栈(stack)是类似于一个先进后出的抽屉.它的 ...
- C#中的值类型和引用类型,深拷贝,浅拷贝
from https://www.jianshu.com/p/2d27b06e253f 一.C#中的值类型和引用类型 概念 值类型直接存储其值. 引用类型存储对值的引用. 说起来有些拗口,其本质是Va ...
- 浅谈C#中的值类型和引用类型
在C#中,值类型和引用类型是相当重要的两个概念,必须在设计类型的时候就决定类型实例的行为.如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常.很多人就是因为没有弄清楚这两个 ...
- 【.Net】浅谈C#中的值类型和引用类型
在C#中,值类型和引用类型是相当重要的两个概念,必须在设计类型的时候就决定类型实例的行为.如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常.很多人就是因为没有弄清楚这两个 ...
- Windows Phone 开发起步之旅之二 C#中的值类型和引用类型
今天和大家分享下本人也说不清楚的一个C#基础知识,我说不清楚,所以我才想把它总结一下,以帮助我自己理解这个知识上的盲点,顺便也和同我一样不是很清楚的人一起学习下. 一说起来C#中的数据类型有哪些,大 ...
- C#中对值类型和引用类型的一点认识
区别值类型和引用类型的重要一点就是值类型赋值的时候是给出一块内存空间,空间里放下要赋给值类型的值.而引用类型是开辟一块内存空间,空间里放下的是要赋给引用类型值的指向地址. 就像一个是复制了银行卡里的现 ...
- js中的值类型和引用类型的区别
1.JavaScript中的变量类型有哪些? (1)值类型(基本类型):字符串(String).数值(Number).布尔值(Boolean).Undefined.Null (这5种基本数据类型是按 ...
- C#中的值类型、引用类型,代码告诉你他是什么类型。
C#代码告诉你这是什么类型. using System; using System.Collections.Generic; using System.Linq; using System.Text; ...
随机推荐
- css之rem布局
rem介绍和原理网上都是,这里不具体介绍 以iphone6设计稿 let htmlWidth = document.documentElement.clientWidth || document.bo ...
- Fish and Oh My Fish in Ubuntu
After install Fish shell, then install Oh My Fish . Oh My Fish(shortly OMF) can make our Fish shell ...
- 【Spring源码解析】—— 策略模式在Spring中的应用
一. 什么是策略模式 策略模式的定义/含义:策略本身就是为了实现某一个目标而采取的一种工作方式,因此只要能够达成目标,则采取哪一种策略都可以:因此多种实际的策略之间是相互平行的. 注意 ...
- JRebel的优势与使用(基于IDEA)
在平时的工作产出中,代码出现问题时往往要不停的修改测试其正确性,每次修改一部分代码都需要重启项目,这十分的耗时,对于企业大型项目来说你重启的时间够你去喝杯咖啡了,本篇博文主要就减少重启项目时间为目的来 ...
- 03_javaSE面试题:类初始化和实例初始化
题目 下面代码运行的结果是什么? Father 类 /** * @author kevin * @date 2019/7/8 15:48 */ public class Father { privat ...
- Vs连接Mysql数据库
Vs连接Mysql数据库步骤 1. 首先下载mysql数据库,安装,建库建表 https://www.yiibai.com/mysql/getting-started-with-mysql-store ...
- C#中产生SQL语句的几种方式
(1)拼接产生SQL语句: string sql = "insert into czyb(yhm,mm,qx) values('" + txtName.Text + "' ...
- c++学习书籍推荐《C++ Primer(中文版)(第5版)》下载
百度云及其他网盘下载地址:点我 编辑推荐 <C++ Primer(中文版)(第5版)>编辑推荐:一书在手,架构无忧:三十位一线架构师真知实践:百位架构师献计献策:十万文字尽显架构精华. 媒 ...
- mimalloc内存分配代码分析
这篇文章中我们会介绍一下mimalloc的实现,其中可能涉及上一篇文章提到的内容,如果不了解的可以先看下这篇mimalloc剖析.首先我们需要了解的是其整体结构,mimalloc的结构如下图所示 ...
- CF510C Fox And Names——拓扑排序练习
省委代码: #include<cstdio> #include<cstdlib> #include<cstring> #include<cmath> # ...