Have you considered performance in interface oriented programming?

Posted by m00p4h on Fri, 17 Dec 2021 16:14:53 +0100

Most of us will follow interface programming in normal development, 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. It depends on your application scenario.

1: Background

1. Reason

During the performance transformation of the project, it is found that the return values of many method signatures adopt 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 does not have any performance problems. foreach iteration is natural. How can this be optimized???

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

See the standard get from IL_ Current, MoveNext, dispose , and try,finally. With so many methods and keywords, isn't it a simple foreach iterative array? As for making it so complicated? How can it get up quickly under big data?

Another wonderful thing is that if you carefully observe the IL code, such as this sentence: [mscorlib] system Collections. Generic. IEnumerable ` ` 1 < int32 >:: getenumerator(), which is preceded by the interface IEnumerable. Under normal circumstances, it should be a specific iterative class. It is reasonable to 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> Finding problems from windbg

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

! clrstack -l > ! Do XX grab the list variable on the 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, however, is the ghost of JIT. It generates such a SZGenericArrayEnumerator type. Next, type its method table to see what methods are 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)

You can see that this is a standard iterative class, which is a drag on performance...

2: Optimize performance

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

1. IEnumerable , replace int [], foreach with for

After knowing these two points, the code is modified 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

You can see that the above IL instructions are very basic instructions. Most of them are directly supported by CPU instructions. They are very concise and love~~~

Here's a point to note: I later observed that foreach does not need to be changed to for. The vs editor helps us convert at the bottom. I can see that foreach is still very intelligent when iterating array types and knows 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

The micro aspect has been analyzed with you. Next, I will test the performance difference between 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
---------------
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

Topics: C# Programmer .NET microsoft