你的位置:首页 > ASP.net教程

[ASP.net教程]完美的.net泛型也有特定的性能黑点?追根问底并且改善这个性能问题


完美的.net真泛型真的完美吗

码C#多年,不求甚解觉得泛型就是传说中那么完美,性能也是超级好,不错,在绝大部分场景下泛型表现简直可以用完美来形容,不过随着前一阵重做IOC时,才发现与自己预想中不一样,觉得自己还是图样图森破,太过拿衣服了

在前面一篇文章(一步一步造个IoC轮子(二),详解泛型工厂)中,我说了泛型工厂带来"接近new的性能",是错误的,我要道歉,其实是完全达不到直接new的性能,差了两个数量级,当然还是比反射速度强很多很多很多


 

性能黑点出在哪里?

我来来演示一下普通类型和泛型的实际测试吧

先来做两个类,一个普通一个泛型

  public class NormalClass  {  }  public class GenericClass<T>  {  }

再来写个循环测试

      var sw = new Stopwatch();      Console.WriteLine("请输入循环次数");      int max = int.Parse(Console.ReadLine());      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = new NormalClass();      }      sw.Stop();      Console.WriteLine("直接创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = new GenericClass<int>();      }      sw.Stop();      Console.WriteLine("泛型创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      Console.ReadLine();

好了,E3CPU,dotnet core 1.0 Release下测试结果(本篇全部测试结果均是E3CPU,dotnet core 1.0 Release模式下测试)

一千万次循环

直接创建耗时3ms,平均每次0.3ns
泛型创建耗时3ms,平均每次0.3ns

表现简单完美啊,顺便一提.net core速度提高了很多,像这样的测试如果在.net 2.0-4.6直接new简单对象一千万次下表现都是30-50ms左右,.net core这个真是提升了一个数量级.

那么我说的性能黑点在哪里了?

问题就在于像泛型工厂这样的代码中,在泛型方法里new 泛型对象,我们继续来段代码测试一下

    public static ISMS Create()    {      return new XSMS();    }    public static ISMS Create<T>() where T : class, ISMS, new()    {      return new T();    }    public static void Main(string[] args)    {      Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);      var sw = new Stopwatch();      Console.WriteLine("请输入循环次数");      int max = int.Parse(Console.ReadLine());      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Create();      }      sw.Stop();      Console.WriteLine("直接创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Create<XSMS>();      }      sw.Stop();      Console.WriteLine("泛型方法创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      Console.ReadLine();    }

以上代码,如果泛型真表现是像我们所预期那么完美,两个测试的时间应该是基本相等才对,那来我们来实际测试一下看吧

一千万次结果

直接创建耗时3ms,平均每次0.3ns
泛型方法创建耗时619ms,平均每次61.9ns

WTF,差异为什么这么大,这是什么回事,200倍啊,传说中泛型不是几乎没有性能损失的么

考虑到这么简单的代码,身经百码的我是不可能写错的,难道这个是泛型实现的问题?看看实际编译出什么鬼再说吧

我们打开一下ILSPY看看IL代码是什么样的,这东西比ildasm用着方便,毕竟我是懒人

原来.net实现这个泛型方法new泛型对象jf 偷了个懒,直接利用编译器加上一句System.Activator.CreateInstance<T>()的方法完事,这个打破了我一直美好的幻想,我以为泛型真的表现得像模板一样完美,JIT时才完全膨胀代码,都是不求甚解导致我的曲解

追根问底,我们再来把new泛型放到泛型内部看看编译后的IL

噢NO,跟泛型方法一样,System.Activator.CreateInstance<T>(),至此我们可以得出结论,new泛型对象都是编译器利用System.Activator.CreateInstance<T>()来做的

性能也就降到跟System.Activator.CreateInstance<T>()的水平了


改善性能黑点

虽然Activator.CreateInstance已经很快了,但本着钻研的精神,我们来尝试加速一下这个创建,至少在泛型中的创建性能,最直接的方法当然是模拟编译后IL代码里直接new普通对象的方法了,怎么处理呢,造一个方法,调用这个方法返回要创建的对象

上代码再说吧

  public class FastActivator<T> where T : class, new()  {    private static readonly Func<T> createFunc = BuildFunc();    private static Func<T> BuildFunc()    {      var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true);      var il = newMethod.GetILGenerator();      il.DeclareLocal(typeof(T));      il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));      il.Emit(OpCodes.Stloc_0);      il.Emit(OpCodes.Ldloc_0);      il.Emit(OpCodes.Ret);                  return newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;    }    public static T CreateInstance()    {      return createFunc();    }  }

在上面的代码中,我们创建一个FastActivator<T>的类,T的约束为class而且有空的构造器方法

static readonly Func<T>这里当访问到这个类时就调用BuildFunc的方法,还记得前面提到的static readonly魔法吗,仅仅调用一次,线程安全

CreateInstance()方法里返回createFunc创建的对象

对于IL代码不了解的同学,我来简单解释一下这段IL Emit的代码吧

var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true); //<-创建一个DynamicMethod 动态方法

var il = newMethod.GetILGenerator();//<-取出ILGenerator对象

il.DeclareLocal(typeof(T));//<-接一来定义一个临时本地变量,类型为T

----------------------------------分隔一下----------------------------------------------------

接下来到IL最核心的代码构建了,如下代码

il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));

OpCodes.Newobj是调用构造方法 等同代码里的new关键字,后面 typeof(T).GetConstructor(Type.EmptyTypes)是取出T的空构造方法,整句IL代码意思等于代码  new XX类型() ,这里的XX是T实际的类型

il.Emit(OpCodes.Stloc_0);

OpCodes.Stloc从MSDN里的解释是:“从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。”,意思是把值存入局部变量,Stloc_0中的0就是第0个变量,即我们刚才在上面定义的那个变量

il.Emit(OpCodes.Ldloc_0);

OpCodes.Ldloc从MSDN里的解释是:“将指定索引处的局部变量加载到计算堆栈上。”,意思是把变量加载到栈上,这里是把索引为0的变量加入栈,在IL代码里基本上都是把参数结果等对加载到栈上做相应操作,写IL代码是脑中要有一个栈的表,临时调用的数据都是存到栈上,然后调用方法时就会把栈的参数一一传给方法,当然这个我说不清楚,加深了解直接用ILSPY和代码相互参照就是了

il.Emit(OpCodes.Ret);

OpCodes.Ret就是最后一步就是返回了等同代码里的Return,即使void类型的方法最后一样也是有个OpCodes.Ret表示当前方法完成并返回,如果栈上有值当然就相当于Return xx了

在上面的代码里new出来的对象(指针引用)先存在了栈顶部,然后我们又取出来存入变量[0]然后又从变量[0]取出来压入栈再返回,是否就表示我直接new了就return也行呢

不错,真的行,把il.Emit(OpCodes.Stloc_0);il.Emit(OpCodes.Ldloc_0);这两句及变量声明il.DeclareLocal(typeof(T));去掉实测完全没有影响,我不知编译器为何都要加上这两句,是不够智能还是兼容,不清楚,反正IL代码执行相当快,加上去掉这两句千万次调用基本上时间表现是一致的

最后一个是newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;是利用方法创建一个泛型委托,让我们可以直接调用委托而不用反射来调用方法

好了,代码准备好了,是驴是马拉出来溜一溜就知道了

测试代码如下

      var sw = new Stopwatch();      Console.WriteLine("请输入循环次数");      int max = int.Parse(Console.ReadLine());      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Create();      }      sw.Stop();      Console.WriteLine("直接创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Create<XSMS>();      }      sw.Stop();      Console.WriteLine("泛型方法创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Activator.CreateInstance<XSMS>();      }      sw.Stop();      Console.WriteLine("Activator创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = FastCreate<XSMS>();      }      sw.Stop();      Console.WriteLine("FastActivator创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      Console.ReadLine();

测试结果

还是一千万次

直接创建耗时3ms,平均每次0.3ns
泛型方法创建耗时582ms,平均每次58.2ns
Activator创建耗时552ms,平均每次55.2ns
FastActivator创建耗时130ms,平均每次13ns

虽然比Activator快了近5倍,比预期直接new的速度还是差了两个数量级,当然在.net2.0-4.6里是一个数量级,WTF究竟慢在哪里了

好吧,参考泛型工厂里,我们用个静态的代理对象,代理对象里面包含个Create方法来创建需要的对象来试试能不能再快点,直接上代码吧

  internal class FastActivatorModuleBuilder  {    public static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DynamicFastTypeCreaterAssembly"), AssemblyBuilderAccess.Run).DefineDynamicModule("DynamicFastTypeCreaterModuleBuilder");    public static int CurrId;  }  public class FastActivator<T> where T : class, new()  {    /*//委托方法    public static readonly Func<T> createFunc = BuildFunc();    private static Func<T> BuildFunc()    {      var newMethod = new DynamicMethod("CreateFunc", typeof(T), Type.EmptyTypes, true);      var il = newMethod.GetILGenerator();      //il.DeclareLocal(typeof(T));      il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));      //il.Emit(OpCodes.Stloc_0);      //il.Emit(OpCodes.Ldloc_0);      il.Emit(OpCodes.Ret);                  return newMethod.CreateDelegate(typeof(Func<T>)) as Func<T>;    }*/    public static T CreateInstance()    {      //return createFunc();      return Creater.Create();//调用Creater对象的Create创造T对象    }    private static readonly ICreater Creater = BuildCreater();    public interface ICreater    {      T Create();    }    private static ICreater BuildCreater()    {      var type = typeof(T);      var typeBuilder = FastActivatorModuleBuilder.ModuleBuilder.DefineType("FastTypeCreater_" + Interlocked.Increment(ref FastActivatorModuleBuilder.CurrId),        TypeAttributes.Class | TypeAttributes.Public, null, new Type[] { typeof(ICreater) });//创建类型,继承ICreater接口      var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);//创建类型的构造方法      var il = ctor.GetILGenerator();//从构造方法取出ILGenerator      il.Emit(OpCodes.Ret);//给构造方法加上最基本的代码(空)      var createMethod = typeBuilder.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.HideBySig |        MethodAttributes.NewSlot | MethodAttributes.Virtual |        MethodAttributes.Final, type, Type.EmptyTypes);//创建接口同名方法      il = createMethod.GetILGenerator();//从方法取出ILGenerator      il.DeclareLocal(type);//定义临时本地变量      il.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));//调用当前新建类型的构造方法      il.Emit(OpCodes.Stloc_0);//栈入变量      il.Emit(OpCodes.Ldloc_0);//变量压栈      il.Emit(OpCodes.Ret);//返回栈顶值,方法完成      typeBuilder.DefineMethodOverride(createMethod, typeof(ICreater).GetMethod("Create"));//跟接口方法根据签名进行绑定      var createrType = typeBuilder.CreateTypeInfo().AsType();//创建类型      return (ICreater)Activator.CreateInstance(createrType);//偷懒用Activator.CreateInstance创造刚刚IL代码搞的ICreater对象,有了这个对象就可以调用对象的Create方法调用我们自己搞的IL代码了    }  }

老规矩,一千万次循环

直接创建耗时3ms,平均每次0.3ns

泛型方法创建耗时596ms,平均每次59.6ns
Activator创建耗时552ms,平均每次55.2ns
FastActivator创建耗时79ms,平均每次7.9ns

一千万次,性能继续有提升,几乎是泛型方法的8倍,算是提高了一个数量级了,实际上在 .net2.0-4.6里已经是同数量级的速度了,不过.net core狠啊,直接new够快,这里性能不如预期的原因,我想了好久,百撕不得骑姐的时候,只能够再码点基础代码来测试了

    public class TestCreater    {      /// <summary>      /// 直接创建      /// </summary>      /// <returns></returns>      public static ISMS Driect()      {        return new XSMS();      }      private interface ICreater      {        ISMS Create();      }      private static readonly ICreater creater = new Creater();      private class Creater : ICreater      {        public ISMS Create()        {          return new XSMS();        }      }      /// <summary>      /// 每次都创建Creater对象用Creater对象来创建      /// </summary>      /// <returns></returns>      public static ISMS InternalCreaterCreater()      {        return new Creater().Create();      }      /// <summary>      /// 使用静态缓存的Creater创建      /// </summary>      /// <returns></returns>      public static ISMS StaticCreaterCreate()      {        return creater.Create();      }    }    public static void Main(string[] args)    {      Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);      var sw = new Stopwatch();      Console.WriteLine("请输入循环次数");      int max = int.Parse(Console.ReadLine());      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = Create();      }      sw.Stop();      Console.WriteLine("直接创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = TestCreater.Driect();      }      sw.Stop();      Console.WriteLine("TestCreater.Driect方法创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = TestCreater.InternalCreaterCreater();      }      sw.Stop();      Console.WriteLine("TestCreater.InternalCreaterCreater方法创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      sw.Restart();      for (var i = 0; i < max; i++)      {        var x = TestCreater.StaticCreaterCreate();      }      sw.Stop();      Console.WriteLine("TestCreater.StaticCreaterCreate方法创建耗时{0}ms,平均每次{1}ns", sw.ElapsedMilliseconds, sw.ElapsedMilliseconds * 1000000M / (decimal)max);      Console.ReadLine();    }

在上面的测试代码中,我造了个TestCreater的类分别来测试不同的方法,分别有直接new对象的,用ICreater代理对象来new 对象的及缓存了ICreater代理对象来new对象的

来跑一跑性能表现吧

老规矩,一千万次循环

直接创建耗时3ms,平均每次0.3ns
TestCreater.Driect方法创建耗时3ms,平均每次0.3ns
TestCreater.InternalCreaterCreater方法创建耗时3ms,平均每次0.3ns
TestCreater.StaticCreaterCreate方法创建耗时89ms,平均每次8.9ns

前面两个方法跟直接new时间完全一致,分不出什么胜负,最后一个和FastActivator吻合,性能表现完全一致,到这里我们可以得出结论了,性能下降的原因是由于引用了代理对象,毕竟要访问内在,所以这个下降也是理所当然的


 

优化结论

到此,泛型这个性能黑点优化算是完成了,如果要近乎直接new的性能,估计只能热更新掉运行时已经JIT过的代码,参考http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time

用这种魔法去提升微乎其微的性能,或者祈求官方在泛型里new不要偷懒在编译期实现,而是放到JIT的时候再去实现,不知会不会引起循环引用的问题

如果对于上面所有的测试代码认为有编译器优化的其实可以用ILSPY看一下IL代码或者最简单的就是在XSMS构造方法里加上计数或者控制台输出就知道这些测试代码是可靠的,没有给编译器优化忽略掉

代码就不附了,上面有,会加入前面的IOC里改善性能

有更理想方法的同学可以留言讨论一下