对于循环列表

时间:2015-04-20 12:26:58

标签: f# control-flow

Poeple经常使用

for i in [0 .. 10] do something

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

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

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

3 个答案:

答案 0 :(得分:12)

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

使用F#interactive的#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循环相同的东西。您可以强制它显式创建延迟序列(最后一种情况),这比列表更快,但仍然很慢。

答案 1 :(得分:7)

给出一些不同类型的答案,但希望对某些

感兴趣

你是正确的,因为在这种情况下,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-loop相比,它相当慢。

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

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

显着改善。

所以窃取此代码,将PR发布到https://github.com/Microsoft/visualfsharp/并沐浴在荣耀中。当然你需要添加单元测试并发出IL代码测试,这对于找到合适的级别来说有点棘手,请查看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#中的效率也很低。

答案 2 :(得分:0)

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

  • 在Fortran中执行'do'循环
  • for var:= expr to expr in Pascal

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

无论如何,一个不错的编译器/解释器应该识别相当频繁的特殊情况[expr1..expr2]并避免计算和存储中间列表。