面向接口編程,你考慮過性能嗎?
摘要:} .method public hidebysig static void Main ( string[] args ) cil managed { // (no C# code) IL_0000: nop // int[] hasEmailCustomerIDList = GetHasEmailCustomerIDList()。} .method public hidebysig static void Main ( string[] args ) cil managed { // (no C# code) IL_0000: nop // int[] hasEmailCustomerIDList = GetHasEmailCustomerIDList()。
大家在平時開發中大多都會遵循接口編程,這樣就可以方便實現依賴注入也方便實現多態等各種小技巧,但這種是以犧牲性能爲代價換取代碼的靈活性,萬物皆有陰陽,看你的應用場景進行取捨。
一:背景
1. 緣由
在項目的性能改造中,發現很多方法簽名的返回值都是採用IEnumerable接口,比如下面這段代碼:
public static void Main(string[] args) { var list = GetHasEmailCustomerIDList(); foreach (var item in list){} Console.ReadLine(); } public static IEnumerable<int> GetHasEmailCustomerIDList() { return Enumerable.Range(1, 5000000).ToArray(); }
2. 有什麼問題
這段代碼乍一看也沒啥什麼性能問題,foreach迭代天經地義,這個還能怎麼優化???
<1> 從MSIL中尋找問題
首先我們儘可能把原貌還原出來,簡化後的MSIL如下。
.method public hidebysig static void Main ( string[] args ) cil managed { IL_0009: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() IL_000e: stloc.1 .try { IL_000f: br.s IL_001a // loop start (head: IL_001a) IL_0011: ldloc.1 IL_0012: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() IL_0017: stloc.2 IL_0018: nop IL_0019: nop IL_001a: ldloc.1 IL_001b: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0020: brtrue.s IL_0011 // end loop IL_0022: leave.s IL_002f } // end .try finally { IL_0024: ldloc.1 IL_0025: brfalse.s IL_002e IL_0027: ldloc.1 IL_0028: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_002d: nop IL_002e: endfinally } // end handler IL_002f: ret } // end of method Program::Main
從IL中看到了標準的 get_Current,MoveNext,Dispose
還有一個 try,finally
,一下子多了這麼多方法和關鍵詞,不就是一個簡單的foreach迭代數組嘛? 至於搞的這麼複雜嘛?這樣在大數據下怎麼快的起來?
還有一個奇葩的事,如果你仔細觀察IL代碼,比如這句: [mscorlib]System.Collections.Generic.IEnumerable``1<int32>::GetEnumerator()
, 這個GetEnumerator前面是接口IEnumerable,正常情況下應該是具體迭代類吧,按理說應該會調用Array的GetEnumerator方法,如下所示。
[Serializable] [ComVisible(true)] [__DynamicallyInvokable] public abstract class Array : ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable { [__DynamicallyInvokable] public IEnumerator GetEnumerator() { int lowerBound = GetLowerBound(0); if (Rank == 1 && lowerBound == 0) { return new SZArrayEnumerator(this); } return new ArrayEnumerator(this, lowerBound, Length); } }
<2> 從windbg中尋找問題
IL中發現的第二個問題我特別好奇,:smile::smile:,我們到託管堆上去看下到底是哪一個具體類調用了 GetEnumerator()
方法。
!clrstack -l > !do xx 到線程棧上抓list變量
0:000> !clrstack -l 000000229e3feda0 00007ff889e40951 *** WARNING: Unable to verify checksum for ConsoleApp2.exe ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 32] LOCALS: 0x000000229e3fede8 = 0x0000019bf33b9a88 0x000000229e3fede0 = 0x0000019be33b2d90 0x000000229e3fedfc = 0x00000000004c4b40 0:000> !do 0x0000019be33b2d90 Name: System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]] MethodTable: 00007ff8e8d36d18 EEClass: 00007ff8e7cf5640 Size: 32(0x20) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ff8e7a98538 4002ffe 8 System.Int32[] 0 instance 0000019bf33b9a88 _array 00007ff8e7a985a0 4002fff 10 System.Int32 1 instance 5000000 _index 00007ff8e7a985a0 4003000 14 System.Int32 1 instance 5000000 _endIndex 00007ff8e8d36d18 4003001 0 ...Int32, mscorlib]] 0 shared static Empty >> Domain:Value dynamic statics NYI 0000019be1893a80:NotInit <<
居然有這麼一個類型 Name: System.SZArrayHelper+SZGenericArrayEnumerator
,然來是JIT搗的鬼,生成了這麼一個SZGenericArrayEnumerator類型,接下來把它的方法表打出來看看裏面都有啥方法。
0:000> !dumpmt -md 00007ff8e8d36d18 EEClass: 00007ff8e7cf5640 Module: 00007ff8e7a71000 Name: System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]] mdToken: 0000000002000a98 File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll BaseSize: 0x20 ComponentSize: 0x0 Slots in VTable: 11 Number of IFaces in IFaceMap: 3 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 00007ff8e7ff2450 00007ff8e7a78de8 PreJIT System.Object.ToString() 00007ff8e800cc60 00007ff8e7c3b9b0 PreJIT System.Object.Equals(System.Object) 00007ff8e7ff2090 00007ff8e7c3b9d8 PreJIT System.Object.GetHashCode() 00007ff8e7fef420 00007ff8e7c3b9e0 PreJIT System.Object.Finalize() 00007ff8e8b99fd0 00007ff8e7ebf388 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]].MoveNext() 00007ff8e8b99f90 00007ff8e7ebf390 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]].get_Current() 00007ff8e8b99f60 00007ff8e7ebf398 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]].System.Collections.IEnumerator.get_Current() 00007ff8e8b99f50 00007ff8e7ebf3a0 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]].System.Collections.IEnumerator.Reset() 00007ff8e8b99f40 00007ff8e7ebf3a8 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]].Dispose() 00007ff8e8b99ef0 00007ff8e7ebf3b0 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]]..cctor() 00007ff8e8b99ff0 00007ff8e7ebf380 PreJIT System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Int32, mscorlib]]..ctor(Int32[], Int32)
可以看到這是一個標準的迭代類,這性能又被拖累了。。。
二:優化性能
綜合上面分析,貌似問題出在了 foreach
和 IEnumerable<int>
這兩個方面。
1. IEnumerable
替換 int[], foreach改成for
知道了這兩點,接下來把代碼修改如下:
public static void Main(string[] args) { var list = GetHasEmailCustomerIDList(); for (int i = 0; i < list.Length; i++) { } Console.ReadLine(); } public static int[] GetHasEmailCustomerIDList() { return Enumerable.Range(1, 5000000).ToArray(); } .method public hidebysig static void Main ( string[] args ) cil managed { // (no C# code) IL_0000: nop // int[] hasEmailCustomerIDList = GetHasEmailCustomerIDList(); IL_0001: call int32[] ConsoleApp2.Program::GetHasEmailCustomerIDList() IL_0006: stloc.0 // for (int i = 0; i < hasEmailCustomerIDList.Length; i++) IL_0007: ldc.i4.0 IL_0008: stloc.1 // (no C# code) IL_0009: br.s IL_0011 // loop start (head: IL_0011) IL_000b: nop IL_000c: nop // for (int i = 0; i < hasEmailCustomerIDList.Length; i++) IL_000d: ldloc.1 IL_000e: ldc.i4.1 IL_000f: add IL_0010: stloc.1 // for (int i = 0; i < hasEmailCustomerIDList.Length; i++) IL_0011: ldloc.1 IL_0012: ldloc.0 IL_0013: ldlen IL_0014: conv.i4 IL_0015: clt IL_0017: stloc.2 IL_0018: ldloc.2 // (no C# code) IL_0019: brtrue.s IL_000b // end loop // Console.ReadLine(); IL_001b: call string [mscorlib]System.Console::ReadLine() // (no C# code) IL_0020: pop // } IL_0021: ret } // end of method Program::Main
可以看到上面的IL指令都是非常基礎的指令,大多都有CPU指令直接提供支持,非常簡潔,大愛~~~
這裏有一點要注意: 我後來觀察foreach不需要改成for,vs編輯器在底層幫我們轉換了,看的出來foreach在迭代數組類型的時候還是非常智能的,知道怎麼幫助我們優化。。。修改代碼如下:
public static void Main(string[] args) { var list = GetHasEmailCustomerIDList(); //for (int i = 0; i < list.Length; i++) { } foreach (var item in list) { } Console.ReadLine(); } .method public hidebysig static void Main ( string[] args ) cil managed { // (no C# code) IL_0000: nop // int[] hasEmailCustomerIDList = GetHasEmailCustomerIDList(); IL_0001: call int32[] ConsoleApp2.Program::GetHasEmailCustomerIDList() IL_0006: stloc.0 // (no C# code) IL_0007: nop // int[] array = hasEmailCustomerIDList; IL_0008: ldloc.0 IL_0009: stloc.1 // for (int i = 0; i < array.Length; i++) IL_000a: ldc.i4.0 IL_000b: stloc.2 // (no C# code) IL_000c: br.s IL_0018 // loop start (head: IL_0018) // int num = array[i]; IL_000e: ldloc.1 IL_000f: ldloc.2 IL_0010: ldelem.i4 // (no C# code) IL_0011: stloc.3 IL_0012: nop IL_0013: nop // for (int i = 0; i < array.Length; i++) IL_0014: ldloc.2 IL_0015: ldc.i4.1 IL_0016: add IL_0017: stloc.2 // for (int i = 0; i < array.Length; i++) IL_0018: ldloc.2 IL_0019: ldloc.1 IL_001a: ldlen IL_001b: conv.i4 IL_001c: blt.s IL_000e // end loop // Console.ReadLine(); IL_001e: call string [mscorlib]System.Console::ReadLine() // (no C# code) IL_0023: pop // } IL_0024: ret } // end of method Program::Main
2. 代碼測試
微觀方面已經帶大家分析過了,接下來宏觀測試兩種方式的性能到底相差多少,每一個方法我都做10次性能對比。
public static void Main(string[] args) { var arr = GetHasEmailCustomerIDArray(); for (int i = 0; i < 10; i++) { var watch = Stopwatch.StartNew(); foreach (var item in arr) { } watch.Stop(); Console.WriteLine($"i={i},時間:{watch.ElapsedMilliseconds}"); } Console.WriteLine("---------------"); var list = arr as IEnumerable<int>; for (int i = 0; i < 10; i++) { var watch = Stopwatch.StartNew(); foreach (var item in list) { } watch.Stop(); Console.WriteLine($"i={i},時間:{watch.ElapsedMilliseconds}"); } Console.ReadLine(); } public static int[] GetHasEmailCustomerIDArray() { return Enumerable.Range(1, 5000000).ToArray(); } i=0,時間:10 i=1,時間:10 i=2,時間:10 i=3,時間:9 i=4,時間:9 i=5,時間:9 i=6,時間:10 i=7,時間:10 i=8,時間:12 i=9,時間:12 --------------- i=0,時間:45 i=1,時間:37 i=2,時間:35 i=3,時間:35 i=4,時間:37 i=5,時間:35 i=6,時間:36 i=7,時間:37 i=8,時間:35 i=9,時間:36
難以置信的是居然有3-4倍的差距。。。這就是用靈活性換取性能的代價:smile::smile::smile:
好了,本篇就說到這裏,希望對你有幫助。
如您有更多問題與我互動,掃描下方進來吧~