完美的.net真泛型真的完美嗎 碼C#多年,不求甚解覺得泛型就是傳說中那麼完美,性能也是超級好,不錯,在絕大部分場景下泛型表現簡直可以用完美來形容,不過隨著前一陣重做IOC時,才發現與自己預想中不一樣,覺得自己還是圖樣圖森破,太過拿衣服了 在前面一篇文章(一步一步造個IoC輪子(二),詳解泛型工廠) ...
完美的.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泛型對象時偷了個懶,直接利用編譯器加上一句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里改善性能
有更理想方法的同學可以留言討論一下