简介

泛型参考资料烂大街,基本资料不再赘述,比如泛型接口/委托/方法的使用,逆变与协变。

泛型好处有如下几点

  1. 代码重用

    算法重用,只需要预先定义好算法,排序,搜索,交换,比较等。任何类型都可以用同一套逻辑
  2. 类型安全

    编译器保证不会将int传给string
  3. 简单清晰

    减少了类型转换代码
  4. 性能更强

    减少装箱/拆箱,泛型算法更优异。

为什么说泛型性能更强?

主要在于装箱带来的托管堆分配问题以及性能损失

  1. 值类型装箱会额外占用内存
            var a = new List<int>()
{
1,2, 3, 4
};
var b = new ArrayList()
{
1,2,3,4
};

变量a:72kb



变量b:184kb

  1. 装箱/拆箱会消耗额外的CPU
	public void ArrayTest()
{
Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10000000; i++)
{
arrayList.Add(i);
_ = (int)arrayList[i];
}
stopwatch.Stop();
Console.WriteLine($"array time is {stopwatch.ElapsedMilliseconds}");
} public void ListTest()
{
Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
List<int> list = new List<int>();
for (int i = 0; i < 10000000; i++)
{
list.Add(i);
_ = list[i];
}
stopwatch.Stop();
Console.WriteLine($"list time is {stopwatch.ElapsedMilliseconds}");
}

如此巨大的差异,无疑会造成GC的管理成本增加以及额外的CPU消耗。

思考一个问题,如果是引用类型的实参。差距还会如此之大吗?

如果差距不大,那我们使用泛型的理由又是什么呢?

开放/封闭类型

CLR中有多种类型对象 ,比如引用类型,值类型,接口类型和委托类型,以及泛型类型。

根据创建行为,他们又被分为开放类型/封闭类型

为什么要说到这个? 泛型的一个有优点就是代码复用,只要定义好算法。剩下的只要往里填就好了。比如List<>开放给任意实参,大家都可以复用同一套算法。

举个例子

  1. 开放类型是指类型参数尚未被指定,他们不能被实例化 List<>,Dictionary<,>,interface 。它们只是搭建好了基础框架,开放不同的实参
            Type it = typeof(ITest);
Activator.CreateInstance(it);//创建失败 Type di = typeof(Dictionary<,>);
Activator.CreateInstance(di);//创建失败
  1. 封闭类型是指类型已经被指定,是可以被实例化 List<string>,String 就是封闭类型。它们只接受特定含义的实参
            Type li = typeof(List<string>);
Activator.CreateInstance(li);//创建成功

代码爆炸

当使用泛型时,在JIT编译阶段,CLR会获取泛型的IL,再T对应的实参替换,生成合适的本机代码。

但这么做有一个缺点,要为每一种不同的泛型类型/方法组合生成,各种各种的本机代码。这将明显增加程序的Assembly,从而损害性能

CLR为了缓解该现象,做了一个特殊的优化:共享方法体

  1. 相同类型实参,共用一套方法

    如果一个Assembly中使用了List<Struct>另外一个Assembly也使用了List<Struct>

    那么CLR只会生成一套本机代码。

  2. 引用类型实参,共用一套方法

    List<String>与List<Stream> 实参都是引用类型,它们的值都是托管堆上的指针引用。因此CLR对指针都可以用同一套方式来操作

    值类型就不行了,比如int与long. 一个占用4字节,一个占用8字节。占用的内存不长不一样,导致无法用同一套逻辑来复用

眼见为实1

示例代码
    internal class Program
{
static void Main(string[] args)
{
var a = new Test<string>();
var b = new Test<Stream>(); Debugger.Break();
}
} public class Test<T>
{
public void Add(T value)
{ }
public void Remove(T value)
{ }
}

变量a:

变量b

仔细观察发现,它们的EEClass完全一致,它们的Add/Remove方法的MethodDesc也完全一直。这印证了上面的说法,引用类型实参引用同一套方法。

眼见为实2

点击查看代码
    internal class Program
{
static void Main(string[] args)
{
var a = new Test<int>();
var b = new Test<long>();
var c = new Test<MyStruct>(); Debugger.Break();
}
} public class Test<T>
{
public void Add(T value)
{ }
public void Remove(T value)
{ }
} public struct MyStruct
{
public int Age;
}

我们再把引用类型换为值类型,再看看它们的方法表。

变量a:



变量b:



变量c:

一眼就能看出,它们的MethodDesc完全不一样。这说明在Assembly中。CLR为泛型生成了3套方法。

细心的朋友可能会发现,引用类型的实参变成了一个叫System.__Canon的类型。CLR 内部使用 System.__Canon 来给所有的引用类型做“占位符”使用

有兴趣的小伙伴可以参考它的源码:coreclr\System.Private.CoreLib\src\System__Canon.cs

为什么值类型无法共用同一套方法?

其实很好理解,引用类型的指针长度是固定的(32位4byte,64位8byte),而值类型的长度不一样。导致值类型生成的底层汇编无法统一处理。因此值类型无法复用同一套方法。

眼见为实

点击查看代码
    internal class Program
{
static void Main(string[] args)
{
var a = new Test<int>();
a.Add(1);
var b = new Test<long>();
b.Add(1); var c = new Test<string>();
c.Add("");
var d = new Test<Stream>();
d.Add(null); Debugger.Break();
}
} public class Test<T>
{
public void Add(T value)
{
var s = value;
}
public void Remove(T value)
{ }
}
//变量a
00007FFBAF7B7435 mov eax,dword ptr [rbp+58h]
00007FFBAF7B7438 mov dword ptr [rbp+2Ch],eax //int 类型步长4 2ch //变量b
00007FFBAF7B7FD7 mov rax,qword ptr [rbp+58h]
00007FFBAF7B7FDB mov qword ptr [rbp+28h],rax //long 类型步长8 28h 汇编不一致 //变量c
00007FFBAF7B8087 mov rax,qword ptr [rbp+58h]
00007FFBAF7B808B mov qword ptr [rbp+28h],rax // 28h //变量d
00007FFBAF7B8087 mov rax,qword ptr [rbp+58h]
00007FFBAF7B808B mov qword ptr [rbp+28h],rax // 28h 引用类型地址步长一致,汇编也一致。

泛型的数学计算

在.NET 7之前,如果我们要利用泛型进行数学运算。是无法实现的。只能通过dynamic来曲线救国

.NET 7中,引入了新的数学相关泛型接口,并提供了接口的默认实现。

https://learn.microsoft.com/zh-cn/dotnet/standard/generics/math

数学计算接口的底层实现

C#层:

相加的操作主要靠IAdditionOperators接口。

IL层:

+操作符被JIT编译成了op_Addition抽象方法

对于int来说,会调用int的实现

System.Int32.System.Numerics.IAdditionOperators

对于long来说,会调用long的实现

System.Int64.System.Numerics.IAdditionOperators

从原理上来说很简单,BCL实现了基本值类型的所有+-*/操作,只要在泛型中做好约束,JIT会自动调用相应的实现。

结论

一路无话,无非打打杀杀。

泛型,用就完事了。就是要稍微注意(硬盘比程序员便宜多了)值类型泛型造成的代码爆炸。

.NET Core 泛型底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  4. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  5. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  6. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  7. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  8. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

  9. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

  10. JDK source 之 LinkedHashMap原理浅谈

    注:本文参考JDK1.7.0_45源码. LinkedHashMap是基于HashMap实现的数据结构,与HashMap主要的不同为每个Entry是使用双向链表实现的,并且提供了根据访问顺序进行排序的 ...

随机推荐

  1. 2024-08-17:用go语言,给定一个从0开始的整数数组nums和一个整数k, 每次操作可以删除数组中的最小元素。 你的目标是通过这些操作,使得数组中的所有元素都大于或等于k。 请计算出实现这个目

    2024-08-17:用go语言,给定一个从0开始的整数数组nums和一个整数k, 每次操作可以删除数组中的最小元素. 你的目标是通过这些操作,使得数组中的所有元素都大于或等于k. 请计算出实现这个目 ...

  2. 【Python】之Mac使用图片识别pytesseract方法报错

    一.前提: python中使用pytesseract图片识别,报错误: pytesseract.pytesseract.TesseractNotFoundError: tesseract is not ...

  3. Mysql table 调整table的字符集和校对规则

    ALTER TABLE `xxxx`.`xxx` CHARACTER SET = utf8mb4 , COLLATE = utf8mb4_0900_ai_ci ;

  4. 17 Python异常处理(捕获异常、抛出异常、自定义异常)

    本篇是 Python 系列教程第 17 篇,更多内容敬请访问我的 Python 合集 当我们编写代码时,可能会遇到各种各样的错误情况,比如除数为零.找不到文件.网络问题等等.为了优雅地处理这些问题,P ...

  5. 用CSS border画一个铅笔

    先上效果图 该例子来自 CSS世界 的书中项目 总结技巧如下: 巧用 border 和 伪元素 来 绘制层叠效果. 使用 transform-origin 来改变元素的轴心 使用 filter:dro ...

  6. 一文轻松搞定 tarjan 算法(二)(附带 tarjan 题单)

    完结篇:tarjan 求割点.点双连通分量.割边(桥)(附 40 道很好的 tarjan 题目). 上一篇(tarjan 求强连通分量,缩点,求边双) tarjan 求割点 还是求强联通分量的大致思路 ...

  7. CoST: 时间序列预测中分离季节趋势特征的对比学习《CoST: CONTRASTIVE LEARNING OF DISENTANGLED SEASONAL-TREND REPRESENTATIONS FOR TIME SERIES FORECASTING》(时序预测、表征学习、对比学习、因果关系、分离趋势季节特征)

    2022/6/18 11:32,简单记录一下随笔(因为不写点东西,根本注意力不集中,看5分钟可能要摸鱼10分钟,还是要写点,突然发现,草稿箱里最早的一篇没写完的博客是去年的7月2日,救命啊,我拖了一年 ...

  8. Angular 18+ 高级教程 – HttpClient

    前言 HttpClient 是 Angular 对 XMLHttpRequest 和 Fetch 的封装. HttpClient 的 DX (Developer Experience) 比 XMLHt ...

  9. 音视频入门-6-ffmpeg小实验-从v4l2层获取PC ubuntu摄像头图像

    0. 进行本代码实验的前提 确保已经在ubuntu内正确安装了ffmpeg 手把手安装教程可以参考我的另一篇博文<音视频入门-4-ffmpeg命令快速体验音视频开发/ ffmpeg编译过程经历的 ...

  10. Atcoder Beginner Contest 367

    A.Shout Everyday \(\text{Diff }43\) 给你 \(24\) 小时制下的 \(A,B,C\) 三个时刻,问 \(A\) 是否在 \([B,C]\) 范围内 考虑到先将 \ ...