2012-05-04 35 views
7

代碼示例:F#「for循環」優化

let foo1 (arr : int[]) = 
    for i = 0 to arr.Length-1 do 
     arr.[i] <- i 

let foo2 (arr : int[]) = 
    for i in [0..arr.Length-1] do 
     arr.[i] <- i 

我認爲這個功能應該是相互等效(在性能方面)。但是,如果我們看看IL上市,我們會看到:

第一功能,15號線,沒有動態分配,沒有try運營商,沒有虛擬電話:

IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: stloc.0 
IL_0003: br.s IL_0011 
// loop start (head: IL_0011) 
    IL_0005: ldarg.0 
    IL_0006: ldloc.0 
    IL_0007: ldloc.0 
    IL_0008: stelem.any [mscorlib]System.Int32 
    IL_000d: ldloc.0 
    IL_000e: ldc.i4.1 
    IL_000f: add 
    IL_0010: stloc.0 

    IL_0011: ldloc.0 
    IL_0012: ldarg.0 
    IL_0013: ldlen 
    IL_0014: conv.i4 
    IL_0015: blt.s IL_0005 
// end loop 

IL_0017: ret 

和第二個 - 幾乎是100線,大量的虛函數的分配/釋放操作,召喚,許多try/Dispose

IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: ldc.i4.1 
IL_0003: ldarg.0 
IL_0004: ldlen 
IL_0005: conv.i4 
IL_0006: ldc.i4.1 
IL_0007: sub 
IL_0008: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [FSharp.Core]Microsoft.FSharp.Core.Operators/OperatorIntrinsics::RangeInt32(int32, int32, int32) 
IL_000d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [FSharp.Core]Microsoft.FSharp.Core.Operators::CreateSequence<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>) 
IL_0012: call class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<!!0> [FSharp.Core]Microsoft.FSharp.Collections.SeqModule::ToList<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>) 
IL_0017: stloc.0 
IL_0018: ldloc.0 
IL_0019: unbox.any class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> 
IL_001e: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
IL_0023: stloc.1 
.try 
{ 
    // loop start (head: IL_0024) 
     IL_0024: ldloc.1 
     IL_0025: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
     IL_002a: brfalse.s IL_003e 

     IL_002c: ldloc.1 
     IL_002d: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
     IL_0032: stloc.3 
     IL_0033: ldarg.0 
     IL_0034: ldloc.3 
     IL_0035: ldloc.3 
     IL_0036: stelem.any [mscorlib]System.Int32 
     IL_003b: nop 
     IL_003c: br.s IL_0024 
    // end loop 

    IL_003e: ldnull 
    IL_003f: stloc.2 
    IL_0040: leave.s IL_005b 
} // end .try 
finally 
{ 
    IL_0042: ldloc.1 
    IL_0043: isinst [mscorlib]System.IDisposable 
    IL_0048: stloc.s 4 
    IL_004a: ldloc.s 4 
    IL_004c: brfalse.s IL_0058 

    IL_004e: ldloc.s 4 
    IL_0050: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    IL_0055: ldnull 
    IL_0056: pop 
    IL_0057: endfinally 

    IL_0058: ldnull 
    IL_0059: pop 
    IL_005a: endfinally 
} // end handler 

IL_005b: ldloc.2 
IL_005c: pop 
IL_005d: ret 

我的問題是爲什麼F#編譯器使用這麼複雜代碼foo2?爲什麼它使用IEnumerable來實現如此微不足道的循環?

回答

12

在如果使用範圍表達第二示例中,將被轉換成正常for循環:

let foo2 (arr : int[]) = 
    for i in 0..arr.Length-1 do 
     arr.[i] <- i 

,成爲相當於foo1

我引述Section 6.3.12 Range Expressions in F# language specs

甲迭代形式爲變種的表達進行序列expr1中..表達式2 待辦事項表達式3有時闡述爲一個簡單的用於環路表達 (§6.5.7)。

然而,你的第二個例子是更喜歡:

let foo2 (arr : int[]) = 
    let xs = [0..arr.Length-1] (* A new list is created *) 
    for i in xs do 
     arr.[i] <- i 

,你已經創建了一個新的列表明確。

+0

我不知道這個'我在0..arr.Length-1做'表達式。非常感謝。 – qehgt

7

您看到的是使用基於索引的枚舉和基於IEnumerable的枚舉之間的標準差異(或者使用C#術語forforeach)。

在第二個示例中,表達式[0..arr.Length-1]正在創建一個集合,而F#正在使用IEnumerable<T>來枚舉這些值。這種枚舉風格的一部分涉及使用實現IDisposableIEnumerator<T>。將生成try/finally塊,以確保在枚舉結束時調用IDisposable::Dispose方法,即使面對異常也是如此。

F#可以優化第二個例子到第一個例子中,並避免所有額外的開銷?他們可以做這種優化。本質上通過範圍表達式窺視,而不是它只是一個簡單的數字範圍,因此生成等效for代碼。

應該F#優化第二個例子。我的投票將是否定的。像這樣的特點往往從外部看起來微不足道,但實際執行它們,更重要的是維護它們,可能相當昂貴。精明的用戶總是可以將他們的代碼轉換回標準的for版本,並避免IEnumerable<T>開銷(如果剖析器顯示它是一個問題)。不執行優化可以讓F#團隊實現其他強大的功能。