列表中的for循环

For loop in list

人们经常使用

for i in [0 .. 10] do something

但是 afaik 创建了一个列表,然后对其进行迭代,在我看来使用

更有意义
for i = 0 to 10 do something

没有创建不必要的列表但具有相同的行为。 我错过了什么吗? (我猜是这样的)

你是对的,写 for i in [0 .. 10] do something 会生成一个列表,它确实有很大的开销。尽管您也可以省略方括号,在这种情况下它只会构建一个惰性序列(而且,事实证明编译器甚至优化了这种情况)。我通常更喜欢写 in 0 .. 100 do,因为它看起来与迭代序列的代码相同。

利用F#交互的#time特性做一个简单的分析:

for i in [ 0 .. 10000000 ] do // 3194ms (yikes!)
  last <- i

for i in 0 .. 10000000 do     // 3ms
  last <- i

for i = 0 to 10000000 do      // 3ms
  last <- i

for i in seq { 0 .. 10000000 } do // 709ms (smaller yikes!)
  last <- i

因此,事实证明编译器实际上将 in 0 .. 10000000 do 优化为与 0 to 10000000 do 循环相同的东西。您可以强制它显式创建惰性序列(最后一种情况),这比列表快,但仍然很慢。

前一种形式需要语言中的特殊构造(对于 var from ... to ... by),这是古代编程语言遵循的方式:

  • 'do' Fortran 中的循环
  • 对于 var:= expr 到 Pascal 中的 expr
  • 等等

后一种形式(对于 var in something)更为通用。它适用于普通列表,但也适用于生成器(如 python)等。在 运行 列表之前可能不需要构建完整列表。这允许在潜在的无限列表上编写循环。

无论如何,一个体面的compiler/interpreter应该能识别出比较频繁的特例[expr1..expr2],避免中间列表的计算和存储。

给出一个有点不同的答案,但希望对某些人感兴趣

您是正确的,因为 F# 编译器在这种情况下无法应用快速循环优化。好消息,F# 编译器是开源的,我们可以改进它的行为。

这是我的免费赠品:

快速循环优化发生在 tastops.fs。目前还比较原始,这是我们改进的好机会。

// Detect the compiled or optimized form of a 'for <elemVar> in <startExpr> .. <finishExpr>  do <bodyExpr>' expression over integers
// Detect the compiled or optimized form of a 'for <elemVar> in <startExpr> .. <step> .. <finishExpr>  do <bodyExpr>' expression over integers when step is positive
let (|CompiledInt32ForEachExprWithKnownStep|_|) g expr = 
    match expr with 
    | Let (_enumerableVar, RangeInt32Step g (startExpr, step, finishExpr), _, 
           Let (_enumeratorVar, _getEnumExpr, spBind,
              TryFinally (WhileLoopForCompiledForEachExpr (_guardExpr, Let (elemVar,_currentExpr,_,bodyExpr), m), _cleanupExpr))) -> 

        let spForLoop = match spBind with SequencePointAtBinding(spStart) -> SequencePointAtForLoop(spStart) |  _ -> NoSequencePointAtForLoop 

        Some(spForLoop,elemVar,startExpr,step,finishExpr,bodyExpr,m)
    | _ -> 
        None

let DetectFastIntegerForLoops g expr = 
    match expr with 
    | CompiledInt32ForEachExprWithKnownStep g (spForLoop,elemVar,startExpr,step,finishExpr,bodyExpr,m) 
         // fast for loops only allow steps 1 and -1 steps at the moment
         when step = 1 || step = -1 -> 
            mkFastForLoop  g (spForLoop,m,elemVar,startExpr,(step = 1),finishExpr,bodyExpr)
    | _ -> expr

这里的问题是 RangeInt32Step 只检测像 0..100..1..10 这样的模式。它错过了例如 [0..10]

让我们介绍另一个匹配这些表达式的活动模式SeqRangeInt32Step

let (|SeqRangeInt32Step|_|) g expr =
    match expr with
    // detect '[n .. m]'
    | Expr.App(Expr.Val(toList,_,_),_,[TType_var _],
                [Expr.App(Expr.Val(seq,_,_),_,[TType_var _],
                          [Expr.Op(TOp.Coerce, [TType_app (seqT, [TType_var _]); TType_var _],
                                    [RangeInt32Step g (startExpr, step, finishExpr)], _)],_)],_)
        when
            valRefEq g toList (ValRefForIntrinsic g.seq_to_list_info) &&
            valRefEq g seq g.seq_vref &&
            tyconRefEq g seqT g.seq_tcr ->
            Some(startExpr, step, finishExpr)

    | _ -> None

你怎么知道这是你需要模式匹配的?我经常采用的方法是,我编写一个具有正确属性的简单 F# 程序,并在编译期间放置一个断点以检查表达式。从中我创建了匹配的模式:

让我们把这两个模式放在一起:

let (|ExtractInt32Range|_|) g expr =
  match expr with
  | RangeInt32Step g range -> Some range
  | SeqRangeInt32Step g range -> Some range
  | _ -> None

CompiledInt32ForEachExprWithKnownStep 更新为使用 ExtractInt32Range 而不是 RangeInt32Step

完整的解决方案是这样的:

let (|SeqRangeInt32Step|_|) g expr =
    match expr with
    // detect '[n .. m]'
    | Expr.App(Expr.Val(toList,_,_),_,[TType_var _],
                [Expr.App(Expr.Val(seq,_,_),_,[TType_var _],
                          [Expr.Op(TOp.Coerce, [TType_app (seqT, [TType_var _]); TType_var _],
                                    [RangeInt32Step g (startExpr, step, finishExpr)], _)],_)],_)
        when
            valRefEq g toList (ValRefForIntrinsic g.seq_to_list_info) &&
            valRefEq g seq g.seq_vref &&
            tyconRefEq g seqT g.seq_tcr ->
            Some(startExpr, step, finishExpr)

    | _ -> None

let (|ExtractInt32Range|_|) g expr =
  match expr with
  | RangeInt32Step g range -> Some range
  | SeqRangeInt32Step g range -> Some range
  | _ -> None

// Detect the compiled or optimized form of a 'for <elemVar> in <startExpr> .. <finishExpr>  do <bodyExpr>' expression over integers
// Detect the compiled or optimized form of a 'for <elemVar> in <startExpr> .. <step> .. <finishExpr>  do <bodyExpr>' expression over integers when step is positive
let (|CompiledInt32ForEachExprWithKnownStep|_|) g expr = 
    match expr with 
    | Let (_enumerableVar, ExtractInt32Range g (startExpr, step, finishExpr), _,
           Let (_enumeratorVar, _getEnumExpr, spBind,
              TryFinally (WhileLoopForCompiledForEachExpr (_guardExpr, Let (elemVar,_currentExpr,_,bodyExpr), m), _cleanupExpr))) -> 

        let spForLoop = match spBind with SequencePointAtBinding(spStart) -> SequencePointAtForLoop(spStart) |  _ -> NoSequencePointAtForLoop 

        Some(spForLoop,elemVar,startExpr,step,finishExpr,bodyExpr,m)
    | _ -> 
        None

使用简单的测试程序

let print v =
    printfn "%A" v

[<EntryPoint>]
let main argv =
    for x in [0..10] do
        print x

    0

在优化之前,相应的 C# 代码看起来像这样(IL 代码更易于检查,但如果不使用它可能有点难以理解):

// Test
[EntryPoint]
public static int main(string[] argv)
{
    FSharpList<int> fSharpList = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(0, 1, 10)));
    IEnumerator<int> enumerator = ((IEnumerable<int>)fSharpList).GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            Test.print<int>(enumerator.Current);
        }
    }
    finally
    {
        IDisposable disposable = enumerator as IDisposable;
        if (disposable != null)
        {
            disposable.Dispose();
        }
    }
    return 0;
}

F# 创建一个列表,然后使用枚举器对其进行迭代。难怪它比经典的 for 循环要慢。

应用优化后,我们得到以下代码:

// Test
[EntryPoint]
public static int main(string[] argv)
{
    for (int i = 0; i < 11; i++)
    {
        Test.print<int>(i);
    }
    return 0;
}

显着改善。

所以窃取此代码,post PR 到 https://github.com/Microsoft/visualfsharp/ and bask in glory. Of course you need to add unit tests and emitted IL code tests which can be somewhat tricky to find the right level for, check this commit 以获得灵感

PS。应该也支持 [|0..10|] seq {0..10}

PS。此外 for v in 0L..10L do print v 以及 for v in 0..2..10 do print v 在 F# 中的实现效率也很低。