Most people in normal development will follow the interface programming, so it is convenient to implement dependency injection and convenient to implement various tricks such as polymorphism, but this is to sacrifice performance at the expense of code flexibility, everything has a Yin and Yang, depending on your application scenario to choose.
A: background
1. Why
In the performance overhaul of the project, we found that many method signatures return values that use the IEnumerable interface, such as this code:
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();
}Copy the code
2. What’s the problem
At first glance, this code does not have any performance problems, foreach iteration is natural, how can this be optimized??
<1> Look for problems in the MSIL
First, we try to restore the original as much as possible. The simplified MSIL is as follows.
.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::MainCopy the code
From IL see the standard get_Current,MoveNext,Dispose and a try,finally, suddenly so many methods and keywords, is not a simple array of iteration foreach? As for making it so complicated? How can you get up fast under big data?
Another weird thing, if you look closely at the IL code, is this: [mscorlib] System. Collections, Generic IEnumerable ` ` 1 < int32 > : : GetEnumerator (), the front is the interface GetEnumerator IEnumerable, A concrete iteration class would normally call Array’s GetEnumerator method, as shown below.
[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); }}Copy the code
<2> Look for problems in WinDBG
The second problem found in IL that I’m particularly curious about is 😄😄. Let’s go to the managed heap and see which concrete class calls GetEnumerator().
! clrstack -l > ! Do xx to grab the list variable on the thread stack
0:00 0 >! 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:00 0 >! 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 <<Copy the code
There is a type Name: System.szarrayhelper +SZGenericArrayEnumerator: SZGenericArrayEnumerator: system.szarrayHelper +SZGenericArrayEnumerator: SZGenericArrayEnumerator
0:00 0 >! 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)Copy the code
As you can see, this is a standard iterative class, which again slows down performance…
Two: optimize performance
Foreach and IEnumerable
are both responsible for foreach and IEnumerable
.
1. IEnumerable int[
With that in mind, let’s change the code as follows:
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.0il_0008: stloc.1 // (no C# code) IL_0009: br.sil_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::MainCopy the code
As you can see above IL instructions are very basic instructions, most of them are directly supported by CPU instructions, very simple, great love ~
One thing to note here: I later observed that foreach didn’t need to be changed to for, and the VS editor did the low-level conversion for us. Foreach was smart enough to iterate over array types to help us optimize… Modify the code as follows:
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.0il_000b: stloc.2 // (no C# code) IL_000c: br.sil_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::MainCopy the code
2. Code testing
We have analyzed the micro aspect. Next, we will test the difference between the performance of the two methods. I will compare the performance of each method for 10 times.
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}, time: {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}, time: {watch. ElapsedMilliseconds}"); } Console.ReadLine(); } public static int[] GetHasEmailCustomerIDArray() { return Enumerable.Range(1, 5000000).ToArray(); } I = 0, time: 10 I = 1, time: 10 I = 2, time: 10 I = 3, time: 9 I = 4, time: 9 I = 5, time: 9 I = 6, time: 10 I = 7, time: 10 I = 8, time: 12 I = 9, time: 12 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 45 I = 1: I = 0, time, time: 37 I = 2, time: 35 I = 3, time: 35 I = 4, time: 37 I = 5, time: 35 I = 6, time: 36 I = 7, time: 37 I = 8, time: 35 I = 9, time: 36Copy the code
It is unbelievable that there is a 3-4 times difference… This is the price of performance for flexibility 😄😄😄
Well, that’s all for this piece, I hope it’s helpful.