首页 > 其他分享 >.NET Core 泛型底层原理浅谈

.NET Core 泛型底层原理浅谈

时间:2024-11-07 14:47:42浏览次数:1  
标签:Core 浅谈 var 类型 Test 泛型 new public

简介

image

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

泛型好处有如下几点

  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
image
变量b:184kb
image

  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}");
	}

image

如此巨大的差异,无疑会造成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,再寻找对应的实参替换,生成合适的本机代码。
但这么做有一个缺点,要为每一种不同的泛型类型/方法组合生成,各种各种的本机代码。这将明显增加程序的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:
image

变量b
image

仔细观察发现,它们的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:
image
变量b:
image
变量c:
image

一眼就能看出,它们的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来曲线救国

image

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

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

数学计算接口的底层实现

C#层:
相加的操作主要靠IAdditionOperators接口。
image

IL层:
+操作符被JIT编译成了op_Addition抽象方法
image

对于int来说,会调用int的实现
System.Int32.System.Numerics.IAdditionOperators
image

对于long来说,会调用long的实现
System.Int64.System.Numerics.IAdditionOperators
image

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

结论

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

标签:Core,浅谈,var,类型,Test,泛型,new,public
From: https://www.cnblogs.com/lmy5215006/p/18529501

相关文章

  • ts-泛型&类型声明文件
    泛型泛型允许我们在定义函数、类或接⼝时,使⽤类型参数来表示未指定的类型,这些参数在具体使⽤时,才被指定具体的类型,泛型能让同⼀段代码适⽤于多种类型,同时仍然保持类型的安全性泛型函数//设置泛型使用<T>,T是自定义名称,在函数中使用T表示该类型functionuser<T>(data:T):T{......
  • 基于Azure DevOps 的 CICD 项目部署(.Net Core)
    使用微软的来进行CICD链接:https://dev.azure.com创建新项目3.创建项目名称4.选择仓库地址5.选择空模板6.创建代理池7.按照以下步骤把代理部署到服务器上8.连接你的服务器9.创建新的文件夹mkdirmyangecdmyagent10.可通过链接下载文件wgethttps://vstsa......
  • EF Core 仓储模式
    数据库:SqlServer为例安装包: 数据库连接DbContextpublicclassTestDbContext:DbContext{publicTestDbContext(DbContextOptions<TestDbContext>options):base(options){}publicDbSet<User>Users{get;set;}}注册数据库上下文builder.S......
  • .Net Core NPOI 导出多级表头
     想要导出这样的表格 数据准备格式   附上源码1usingNPOI.HSSF.UserModel;2usingNPOI.SS.UserModel;3usingNPOI.SS.Util;4usingSystem.Data;5usingSystem.Text.RegularExpressions;67namespaceTestConsoleApp8{9//......
  • 如何使用nssm将asp.net core/.net6/.net8的webapi项目、mvc项目、控制台项目等注册为w
    nssm工具可以将asp.netFramework、asp.netcore、net6、.net8、.net10及后续本的的webapi项目、mvc项目、控制台项目、winform项、WPF项目等注册为windows服务。不仅限于上面这些,nssm可以将所有windows可执行文件注册为windows服务。下面,使用nssm将asp.net8的webapi项目注册为w......
  • CentOS 7 下安装部署.NET Core多版本环境
    文章目录一、前言1、什么时候需要配置多环境?2、环境信息二、部署过程1、SDK下载&安装基础依赖2、配置环境入口三、问题一、前言1、什么时候需要配置多环境?.NETCore作为.NET的开源版本,可以说是非常拥抱开源了,更新的力度也非常给力。却也从开源世界学了点坏东西,那......
  • .net core 项目使用log4net
    引入nuget包 1.增加一个log4Net.config的文件<?xmlversion="1.0"encoding="utf-8"?><log4net><appendername="DebugAppender"type="log4net.Appender.DebugAppender"><layouttype="log4n......
  • .net core 使用定时任务 quartz.net 实例
    项目是core3.1的 只用引用一个包就可以了引用配置Quartz首先创建一个任务工厂publicclassCronJobFactory:IJobFactory{privatereadonlyIServiceProvider_serviceProvider;publicCronJobFactory(IServiceProviderserviceProvider){_ser......
  • 在不受支持的 Mac 上安装 macOS Sequoia (OpenCore Legacy Patcher v2.1.0)
    在不受支持的Mac上安装macOSSequoia(OpenCoreLegacyPatcherv2.1.0)InstallmacOSonunsupportedMacs请访问原文链接:在不受支持的Mac上安装macOS(索引页面)查看最新版。原创作品,转载请保留出处。作者主页:sysin.org2024-11-01,OpenCoreLegacyPatcher2.1.0发......
  • TypeScript中的类型注解、Interface接口、泛型
    一、认识TypeScript1.概述TypeScript是具有类型语法的JavaScript,是一门强类型的编程语言。它是JavaScript的超集(js中的所有元素都属于ts),这意味着任何有效的JavaScript代码本身也是有效的TypeScript代码。2.优点静态类型检查TypeScript在编译阶段就可以检查类......