舟山天气2345:面向接口编程,你思量过性能吗?

admin 5个月前 (05-03) 科技 37 0

人人在平时开发中大多都市遵照接口编程,这样就可以利便实现依赖注入也利便实现多态等种种小技巧,但这种是以牺牲性能为价值换取代码的灵活性,万物皆有阴阳,看你的应用场景举行取舍。

一:靠山

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中发现的第二个问题我稀奇好奇,,我们到托管堆上去看下到底是哪一个详细类挪用了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)

可以看到这是一个尺度的迭代类,这性能又被拖累了。。。

二:优化性能

综合上面剖析,貌似问题出在了 foreachIEnumerable<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倍的差距。。。这就是用灵活性换取性能的价值

好了,本篇就说到这里,希望对你有辅助。

如您有更多问题与我互动,扫描下方进来吧~

,

Sunbet

www.006yb.com是Sunbet公司指定亚洲官方直营现金网,官方授权,老品牌信誉有保障.Sunbet欢迎您加入我们。

皇冠APP声明:该文看法仅代表作者自己,与本平台无关。转载请注明:舟山天气2345:面向接口编程,你思量过性能吗?

网友评论

  • (*)

最新评论

站点信息

  • 文章总数:533
  • 页面总数:0
  • 分类总数:8
  • 标签总数:922
  • 评论总数:163
  • 浏览总数:9384