您当前的位置:首页 > 文章 > C#(含Unity)unsafe指针快速

C#(含Unity)unsafe指针快速反射第五篇(性能比较篇 )

作者:流觞曲水醉江蛟 时间:2023-09-18 阅读数:178 人阅读

C#(含Unity)unsafe指针快速反射第一篇(字段篇 )

C#(含Unity)unsafe指针快速反射第二篇(属性篇 )

C#(含Unity)unsafe指针快速反射第三篇(数组篇 )

C#(含Unity)unsafe指针快速反射第四篇(整合篇 )


Unity3D(已完成iL2cpp和Mono的编译)请访问:smopu/unity3d_quick_reflection: 在unity3D中使用指针进行快速反射 (github.com)

.Net Framework 和.Net Core请访问:smopu/CSharp_QuickReflection: C# .Net Framework Quick Reflection 快速反射 (github.com)

本次测试的Unity3D源代码在 smopu/UnityReflectionCompare: Unity下各种反射方式的性能比较 (github.com)

本次测试的.Net 源代码在 smopu/ReflectionCompare: 各种C#反射方式的性能比较 (github.com)

感谢冰封百度提供的帮助,我这里修改了他在博客上的Emit技术、表达式树技术的使用代码,博客原文Unity C# 反射性能优化 - 冰封百度的学习笔记 - SegmentFault 思否

感谢莫明棋妙对Unity平台的测试帮助,他的Git地址:github.com/ajycode

现在开始正式排位,啊不是,开始正式比较性能,在比赛之前,我跟各位观众约定一下本次测试的规则:

本次赛场分为.Net赛场和Unity赛场。我们把所有参赛选手都通过release条件下编译成Dll,然后测试工程使用一个MyClass类,测试工程在执行的时候分debug环境和release环境分别测试,主要以debug环境的结果为主,因为实际项目开发中,大部分项目都使用debug环境,只有部分功能库和插件会用release环境,而反射库是一个基本功能库,所以我们所有参赛选手使用release环境。

本次参赛选手有三位:指针反射PtrReflection,动态编译Emit,表达式树Expression。同时我们有三个对照组选手,1:原生反射System.Reflection,2:直接C#赋值和取值(后面展示源代码),3:通过switch判断字段/属性名字符串来赋值和取值(后面展示源代码)。

三个对照组选手的源代码不参与release条件编译。

本次比赛的MyClass类源代码公示:

 public class MyClass
    {
        public int one;
        public string str;
        public Vector3 point;

        public int One { get; set; }
        public string Str { get; set; }
        public Vector3 Point { get; set; }


        public int[,,] oness;
        public string[,] strss;
        public Vector3[,] pointss;

        public int[] ones;
        public string[] strs;
        public Vector3[] points;


        public static int staticOne = 11;
        public static string staticString = "ADc%";
        public static Vector3 staticVector3 = new Vector3(1.1f, 2.2f, 3.3f);
    }

参赛选手将要对 one、str、point三个字段以及One、Str、Point三个属性进行赋值和取值,字段和属性分开比较,同时还加入不确定类型(object类型)和确定类型的双重比较,循环1000000比较耗时,耗时少的参赛选手将胜出,但是考虑到程序每次执行耗时都会略有不同,所以耗时相差在百分之十之内视为平局,耗时相差的比较方法为:耗时较高的时间/耗时较低的时间-100%

不确定类型(object类型)比较方式就是把字段和属性只看做object类型赋值和取值,确定类型比较方式是在赋值和取值时能确定字段和属性的类型。

本次比赛所有参赛选手在赋值和取值时,有权知道反射的内容是字段还是属性,就像原生反射的那样:FieldInfo和PropertyInfo所区分的那样。

本次比赛以debug环境为主,release环境为参考环境,只有当debug环境下双方选手为平局时,才考虑release环境。debug环境执行三次展示耗时,release执行一次展示耗时。

三名参赛选手指针反射PtrReflection,动态编译Emit,表达式树Expression都有各种的Wrapper对象,我们把Wrapper对象的实例化不计入耗时比较中。每1个需要反射的类对应一个Wrapper对象,在实际工程中第一次反射某个类时需要先生成再通过Wrapper对象,后面再反射这个类的时候只需要1次查找把对应Type的Wrapper对象找到,因为Wrapper对象每个类都只需要生成一次,所以本次比赛不考虑它生成的时间,而且本次比赛三位选手都把Wrapper对象提前生成,所以不影响比赛结果。我们的三个对照组选手均没有Wrapper对象。

三位参赛选手的Wrapper对象如下:

TypeAddrReflectionWrapper reflectionWrapper = new TypeAddrReflectionWrapper(typeof(MyClass));

EmitWrapperType emitWrapperType = new EmitWrapperType(typeof(MyClass));

ExpressionWrapperType expressionWrapperType = new ExpressionWrapperType(typeof(MyClass));

现在展示“直接C#赋值和取值”的源代码

obj.one = 13;
v1 = obj.one;

obj.str = "sad2";
v2 = obj.str;

obj.point = new Vector3(1.1f, 2.2f, 3.3f);
v3 = obj.point; 

下面是"switch"选手的源代码:

 obj.one = 13;
v1 = obj.one;

obj.str = "sad2";
v2 = obj.str;

obj.point = new Vector3(1.1f, 2.2f, 3.3f);
v3 = obj.point;        static void SwitchSetData(MyClass myClass, object data, string name) 
{
    switch (name)
    {
        case "one":
            myClass.one = (int)data;
            break;
        case "str":
            myClass.str = (string)data;
            break;
        case "point":
            myClass.point = (Vector3)data;
            break;
        case "One":
            myClass.One = (int)data;
            break;
        case "Str":
            myClass.Str = (string)data;
            break;
        case "Point":
            myClass.Point = (Vector3)data;
            break;
        case "oness":
            myClass.oness = (int[,,])data;
            break;
        case "strss":
            myClass.strss = (string[,])data;
            break;
        case "pointss":
            myClass.pointss = (Vector3[,])data;
            break;
        case "ones":
            myClass.ones = (int[])data;
            break;
        case "strs":
            myClass.strs = (string[])data;
            break;
        case "points":
            myClass.points = (Vector3[])data;
            break;
    }
}

static object SwitchGetData(MyClass myClass, string name)
{
    switch (name)
    {
        case "one":
            return myClass.one;
        case "str":
            return myClass.str;
        case "point":
            return myClass.point;
        case "One":
            return myClass.One;
        case "Str":
            return myClass.Str;
        case "Point":
            return myClass.Point;
        case "oness":
            return myClass.oness;
        case "strss":
            return myClass.strss;
        case "pointss":
            return myClass.pointss;
        case "ones":
            return myClass.ones;
        case "strs":
            return myClass.strs;
        case "points":
            return myClass.points;
        default:
            return null;
    }
}

我们可以看到,这就是直接通过字符串来赋值取值的硬编码。

对规则有疑问的观众请前往评论区发表您的观点,您的参与将有机会获得额外大礼!!!

下面进行.Net赛区的debug环境比较,以下是三次耗时:

最后一次的成绩,我把文本给出来

C#, 字段: 13.1569 毫秒

switch, 字段: 470.486 毫秒


PtrReflection, 字段, object类型: 186.0537 毫秒

Emit, 字段, object类型: 248.2762 毫秒

Expression, 字段, object类型: 227.9534 毫秒

System.Reflection, 字段, object类型: 1092.092 毫秒


PtrReflection, 属性, object类型: 162.2727 毫秒

Emit, 属性, object类型: 206.9175 毫秒

Expression, 属性, object类型: 207.5198 毫秒

System.Reflection, 属性, object类型: 1966.1938 毫秒


PtrReflection, 字段,指定类型(值类型无装箱,传参无拷贝):92.1995 毫秒

Emit, 字段, 指定类型(值类型无装箱): 173.3449 毫秒

Expression, 字段, 指定类型(值类型无装箱): 149.5582 毫秒


PtrReflection, 属性,指定类型(值类型无装箱): 122.3596 毫秒

Emit, 属性, 指定类型(值类型无装箱): 183.5979 毫秒

Expression, 属性, 指定类型(值类型无装箱): 207.9753 毫秒

下面进行.Net赛区的release环境比较,以下是1次耗时:

C#, 字段: 2.9452 毫秒

switch, 字段: 97.3043 毫秒


PtrReflection, 字段, object类型: 108.2992 毫秒

Emit, 字段, object类型: 223.9893 毫秒

Expression, 字段, object类型: 290.535 毫秒

System.Reflection, 字段, object类型: 1219.6261 毫秒


PtrReflection, 属性, object类型: 110.1392 毫秒

Emit, 属性, object类型: 137.3991 毫秒

Expression, 属性, object类型: 166.3366 毫秒

System.Reflection, 属性, object类型: 1862.1221 毫秒


PtrReflection, 字段,指定类型(值类型无装箱,传参无拷贝):31.7233 毫秒

Emit, 字段, 指定类型(值类型无装箱): 129.0019 毫秒

Expression, 字段, 指定类型(值类型无装箱): 119.0279 毫秒


PtrReflection, 属性,指定类型(值类型无装箱): 43.4737 毫秒

Emit, 属性, 指定类型(值类型无装箱): 122.3448 毫秒

Expression, 属性, 指定类型(值类型无装箱): 158.7886 毫秒

有观众可能疑惑,为什么switch硬编码耗时在debug环境里面反而这么慢?因为switch在debug环境中效率就是很低,跟字符串字典查询差不多,再加上转换为object的装箱操作,所以就比三位参赛选手都慢,而switch硬编码在release里面才是真正提高了效率,然而在relesase环境下仍然比不过指针反射PtrReflection,原因是因为指针反射用了更高效的字符串匹配算法。(在本人后面的文章会讲解更高效的字符串匹配算法)

其他两个参照组还是不出意料,C#直接读写的速度肯定是最快的,而原生的反射是最慢的。

三位选手在object类型比较中其实成绩差不多,Emit综合而言效率最低。在指定类型比较中指针反射效率拉开了其他两位选手一段距离,原因是指针反射传参时(值类型传参会拷贝参数)无拷贝,直接指针操作地址赋值。

在release模式,指针反射完美胜出。但我们不考虑release模式。

Unity赛场理论上我们还会考虑AOT环境下的问题,也就是IL2Cpp、iOS、WebGL等平台。但是Expression和Emit都用到及时编译(JIT)技术,导致AOT环境下不能使用。

现在开始Unity赛场的比赛,Unity赛场,采用编辑器模式和mono编译模式,由于Expression和Emit在IL2Cpp中不能使用,所以不采取IL2Cpp编译模式。

第一次比较:

第二次比较

第三次比较

第三次比较结果的文本:

C#, 字段: 1.6263 毫秒

switch, 字段: 113.5346 毫秒


PtrReflection, 字段, object类型: 158.5009 毫秒

Emit, 字段, object类型: 162.632 毫秒

Expression, 字段, object类型: 146.8331 毫秒

System.Reflection, 字段, object类型: 1678.3367 毫秒


PtrReflection, 属性, object类型: 174.0234 毫秒

Emit, 属性, object类型: 166.9311 毫秒

Expression, 属性, object类型: 174.8529 毫秒

System.Reflection, 属性, object类型: 2459.9966 毫秒


PtrReflection, 字段,指定类型(值类型无装箱,传参无拷贝):32.1415 毫秒

Emit, 字段, 指定类型(值类型无装箱): 77.8912 毫秒

Expression, 字段, 指定类型(值类型无装箱): 79.1044 毫秒


PtrReflection, 属性,指定类型(值类型无装箱): 33.6443 毫秒

Emit, 属性, 指定类型(值类型无装箱): 86.6389 毫秒

Expression, 属性, 指定类型(值类型无装箱): 86.7509 毫秒

下面是mono编译成exe执行后的比较结果:

可以看到,在Unity赛场,指针反射在Object类型读写属性的效率上还是比较落后,但综合实力仍然领先。

目前,本人宣布C#第一届反射效率比赛.Net赛场比赛结果:指针反射PtrReflection荣获冠军,表达式树Expression荣获亚军,动态编译Emit荣获季军!让我们恭喜各位选手!

想要报名参赛的选手请前往评论区留言,您的报名将会获得惊喜!!!

本站大部分文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了您的权益请来信告知我们删除。邮箱:1451803763@qq.com