8

代码示例:

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?为什么它使用 anIEnumerable来实现如此微不足道的循环?

4

2 回答 2

14

在第二个示例中,如果您使用范围表达式,它将被转换为正常for循环:

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

并变得等价于foo1

我在 F# 语言规范中引用了第 6.3.12 节范围表达式

for var in expr1 .. expr2 do expr3 done 形式的序列迭代表达式有时被详细说明为简单的 for 循环表达式(第 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

您在其中明确创建了一个新列表。

于 2012-05-04T15:48:58.987 回答
8

您看到的是使用基于索引的枚举和IEnumerable基于枚举(或 C# 术语forvs foreach)之间的标准区别。

在第二个示例中,表达式[0..arr.Length-1]正在创建一个集合,并且 F#IEnumerable<T>用于枚举值。这种枚举风格的一部分涉及使用IEnumerator<T>which implements IDisposable。生成您所看到的try / finally块以确保IDisposable::Dispose在枚举结束时调用该方法,即使在遇到异常时也是如此。

F# 能否将第二个示例优化为第一个示例并避免所有额外开销?他们有可能进行这种优化。本质上是查看范围表达式,而不仅仅是一个简单的数字范围,因此会生成等效for代码。

F# 是否应该优化第二个示例。我的投票是否定的。像这样的功能通常从外部看起来微不足道,但实际上实现它们,更重要的是维护它们,可能会相当昂贵。精明的用户总是可以将他们的代码转换回标准for版本并避免IEnumerable<T>开销(如果分析器发现这是一个问题)。不实施优化可以让 F# 团队腾出时间来实施其他很棒的功能。

于 2012-05-04T15:49:04.790 回答