上接 DotNet並行計算的使用誤區(一)
● 誤區三 . 並行計算是運行時的事
的確,DotNet會在運行時決定是否使用並行庫處理代碼,但是早在你編譯代碼時,編譯器就早已為這一時刻做好準備,換就話說:
1. 使用並行庫處理代碼與普通方式對比,IL的結構是不同的。
2. 即使你選擇使用並行計算,並且你也確實擁有多核(線程)CPU,運行時你的代碼也不一定是並行的。
使用TPL後CLR可能會分解任務,這一依據的其中之一是由IL支援的,IL將並行的任務代碼分離,以便在將來的操作中並行,這一點可以從以下的樣本中看出來,以下兩段樣本的核心C#代碼都是Tostring()和Sleep(),Code A使用For包含Sleep,Code B使用Parallel.For處理:
Code Part A:
IL:
IL_000e: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_0013: nop
IL_0014: ldc.i4.0
IL_0015: stloc.2
IL_0016: br.s IL_0031
IL_0018: nop
IL_0019: ldloca.s i
IL_001b: call instance string [mscorlib]System.Int32::ToString()
IL_0020: stloc.0
IL_0021: ldc.i4 0xc8
IL_0026: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_002b: nop
IL_002c: nop
IL_002d: ldloc.2
IL_002e: ldc.i4.1
IL_002f: add
IL_0030: stloc.2
IL_0031: ldloc.2
IL_0032: ldc.i4.s 10
IL_0034: clt
IL_0036: stloc.3
IL_0037: ldloc.3
IL_0038: brtrue.s IL_0018
IL_003a: ldloc.1
IL_003b: callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
我們注意到,Code Part A的Sleep是直接出現在Load方法中的。
再來看看Parallel方式:
Code Part B:
Form1_Load:
IL_0019: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_001e: nop
IL_001f: ldc.i4.0
IL_0020: ldc.i4.s 10
IL_0022: ldloc.1
IL_0023: ldftn instance void WindowsFormsApplication4.Form1/'<>c__DisplayClass1'::'<Form1_Load>b__0'(int32)
IL_0029: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object,
native int)
IL_002e: call valuetype [mscorlib]System.Threading.Tasks.ParallelLoopResult [mscorlib]System.Threading.Tasks.Parallel::For(int32,
int32,
class [mscorlib]System.Action`1<int32>)
IL_0033: pop
IL_0034: ldloc.0
IL_0035: callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
注意紅色字型,Sleep已經不在Load方法中了,而是被一個“b__0”代替,並行代碼與宿主代碼分離,以下就是b__0的IL:
.method public hidebysig instance void '<Form1_Load>b__0'(int32 i) cil managed
{
// 代碼大小 26 (0x1a)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarga.s i
IL_0004: call instance string [mscorlib]System.Int32::ToString()
IL_0009: stfld string WindowsFormsApplication4.Form1/'<>c__DisplayClass1'::a
IL_000e: ldc.i4 0xc8
IL_0013: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_0018: nop
IL_0019: ret
} // end of method '<>c__DisplayClass1'::'<Form1_Load>b__0'
結構圖:
以上的紅色代碼就是在Code A中出現的主要代碼。再讓我們重溫一下這張圖,IL的代碼任務已經很明顯的指示了出來。
每當我們增加一個並行程式碼片段,IL中就會增加一個b_N塊:假如我們的代碼中包含兩個Parallel塊,每塊的主代碼與上述一致,IL如下:
IL_0019: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_001e: nop
IL_001f: ldc.i4.0
IL_0020: ldc.i4.s 10
IL_0022: ldloc.1
IL_0023: ldftn instance void WindowsFormsApplication4.Form1/'<>c__DisplayClass2'::'<Form1_Load>b__0'(int32)
IL_0029: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object,
native int)
IL_002e: call valuetype [mscorlib]System.Threading.Tasks.ParallelLoopResult [mscorlib]System.Threading.Tasks.Parallel::For(int32,
int32,
class [mscorlib]System.Action`1<int32>)
IL_0033: pop
IL_0034: ldc.i4.0
IL_0035: ldc.i4.s 10
IL_0037: ldloc.1
IL_0038: ldftn instance void WindowsFormsApplication4.Form1/'<>c__DisplayClass2'::'<Form1_Load>b__1'(int32)
IL_003e: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object,
native int)
IL_0043: call valuetype [mscorlib]System.Threading.Tasks.ParallelLoopResult [mscorlib]System.Threading.Tasks.Parallel::For(int32,
int32,
class [mscorlib]System.Action`1<int32>)
IL_0048: pop
IL_0049: ldloc.0
IL_004a: callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
中會有對應模組出現:
上面的例子說明,在IL階段已經為運行時的並存執行任務做了準備,編譯階段將並行任務從宿主中分離出來,運行階段決定是否採用並行方式執行任務。