Have you considered performance in interface oriented programming?

Keywords: Programming Windows Big Data

In normal development, most of you will follow the interface programming, which can facilitate the implementation of dependency injection, polymorphism and other small skills, but this is at the expense of performance in exchange for code flexibility. Everything has Yin and Yang. See your application scenarios for choices.

1: Background

1. Reason

In the performance transformation of the project, it is found that the return values of many method signatures are all based on the IEnumerable interface, such as the following 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();
        }

2. What's the problem

At first glance, this code doesn't have any performance problems. foreach iteration is natural. How can this be optimized???

<1> Looking for problems from MSIL

First, we try to restore the original appearance 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::Main

From the IL, we can see that the standard get īšŖ current, MoveNext, dispose also has a try,finally. There are so many methods and keywords all at once. Isn't it just a simple foreach iteration array? As for the complexity? How can it get up quickly under big data?

There is another wonderful thing. If you carefully observe the IL code, such as this sentence: [mscorlib] system. Collections. Generic. IEnumerable ` ` 1 < int32 >:: getenumerator(), the GetEnumerator is preceded by the interface IEnumerable. Normally, it should be a specific iteration class. It should call the GetEnumerator method of Array, 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);
	}
}

<2> Looking for problems from windbg

The second question I found in IL is very curious, 😄😄 , let's go to the managed heap to see which specific class called the GetEnumerator() method.

! clrstack - L >! Do XX grab list variable on thread stack


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  <<

There is such a type Name: System.SZArrayHelper+SZGenericArrayEnumerator, but it's the ghost of JIT. It generates such a SZGenericArrayEnumerator type. Next, type its method table to see what's in it.


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)

As you can see, this is a standard iteration class, and this performance has been dragged down again...

2: Optimize performance

Based on the above analysis, it seems that the problem lies in foreach and IEnumerable < int >.

1. IEnumerable < int > replace int [], foreach change to for

Now that you know these two points, modify 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.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

It can be seen that the above IL instructions are very basic instructions. Most of them are directly supported by CPU instructions. They are very simple and love each other~~~

Here's one thing to note: I later observed that foreach didn't need to be changed to for, and the vs editor helped us transform it at the bottom. It can be seen that foreach is still very intelligent when iterating array types, and I know how to help us optimize... The modification code is 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.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. Code test

In the micro aspect, I've taken you through the analysis. Next, I'll test the performance difference between the two methods. I'll make 10 performance comparisons for each method.

        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
---------------
i=0,time:45
i=1,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:36

It's unbelievable that there's a gap of 3-4 times... That's the price of flexibility for performance 😄😄😄

Well, that's all. I hope it can help you.

Posted by yhingsmile on Sun, 03 May 2020 04:28:00 -0700