java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫


java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

前言:大家在平時開發中大多都會遵循接口編程,這樣就可以方便實現依賴注入也方便實現多態等各種小技巧。有沒有想過我們天天寫面向接口編程,有沒有考慮過性能問題呢?

java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

但這種是以犧牲性能為代價換取代碼的靈活性,萬物皆有陰陽,看你的應用場景進行取捨。

這篇文章就介紹了面向對象編程中,如何讓性能更好,更快,帶你看看跟大神的差距在哪裡?

話不多說,咱們進入正題,要不然各位小夥伴等不及了。接下來就按住你們躁動的心靈,耐心看完!

java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

一:背景

1. 緣由

在項目的性能改造中,發現很多方法簽名的返回值都是採用IEnumerable接口,比如下面這段代碼:

<code>
public static void Main(string[] args)
{
var list = GetHasEmailCustomerIDList();

foreach (var item in list){}

Console.ReadLine();
}

public static IEnumerable GetHasEmailCustomerIDList()
{
return Enumerable.Range(1, 5000000).ToArray();
}
/<code>

2. 有什麼問題?

這段代碼乍一看也沒啥什麼性能問題,foreach迭代天經地義,這個還能怎麼優化???

java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

<1> 從MSIL中尋找問題

首先我們儘可能把原貌還原出來,簡化後的MSIL如下。

<code>
.method public hidebysig static
void Main (
string[] args
) cil managed
{
IL_0009: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 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/<int32>/<int32>/<code>

從IL中看到了標準的get_Current,MoveNext,Dispose 還有一個try,finally,一下子多了這麼多方法和關鍵詞,不就是一個簡單的foreach迭代數組嗎? 至於搞的這麼複雜嘛?這樣在大數據下怎麼快起來?三連問。

java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

還有一個奇葩的事,如果你仔細觀察IL代碼,比如這句:[mscorlib]System.Collections.Generic.IEnumerable``1<int32>::GetEnumerator(), 這個GetEnumerator前面是接口IEnumerable,正常情況下應該是具體迭代類吧,按理說應該會調用Array的GetEnumerator方法,如下所示。/<int32>

<code>[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);
}
}/<code>

<2> 從windbg中尋找問題

IL中發現的第二個問題我特別好奇,我們到託管堆上去看下到底是哪一個具體類調用了GetEnumerator()方法。

!clrstack -l > !do xx 到線程棧上抓list變量

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

居然有這麼一個類型 Name: System.SZArrayHelper+SZGenericArrayEnumerator,原來是JIT搗的鬼,生成了這麼一個SZGenericArrayEnumerator類型,接下來把它的方法表敲出來看看裡面都有啥方法。

<code> 

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)/<code>

可以看到這是一個標準的迭代類,這性能又被拖累了。。。

二:優化性能

java架構:天天寫面向接口編程,你考慮過性能嗎?大神都是這麼寫

綜合上面分析,貌似問題出在了 foreach 和 IEnumerable這兩個方面。

1. IEnumerable 替換 int[], foreach改成for

知道了這兩點,接下來把代碼修改如下:

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

可以看到上面的IL指令都是非常基礎的指令,大多都有CPU指令直接提供支持,非常簡潔,大愛~

這裡有一點要注意: 我後來觀察foreach不需要改成for,vs編輯器在底層幫我們轉換了,看的出來foreach在迭代數組類型的時候還是非常智能的,知道怎麼幫助我們優化。。。修改代碼如下:

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

2. 代碼測試

微觀方面已經帶大家分析過了,接下來宏觀測試兩種方式的性能到底相差多少,每一個方法我都做10次性能對比。

<code>        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;
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
/<code>

難以置信的是居然有3-4倍的差距。。。這就是用靈活性換取性能的代價

好了,本篇就說到這裡,希望對你有幫助。


分享到:


相關文章: