繁体   English   中英

F#Seq的实现问题

[英]An implementation problem of F# Seq

我最近正在深入研究F#源代码。

在Seq.fs中:

// Binding. 
//
// We use a type defintion to apply a local dynamic optimization. 
// We automatically right-associate binding, i.e. push the continuations to the right.
// That is, bindG (bindG G1 cont1) cont2 --> bindG G1 (cont1 o cont2)
// This makes constructs such as the following linear rather than quadratic:
//
//  let rec rwalk n = { if n > 0 then 
//                         yield! rwalk (n-1)
//                         yield n }

看到上面的代码后,我测试了两个代码:

let rec rwalk n = seq { if n > 0 then 
                         yield n
                         yield! rwalk (n-1)
                      }

let rec rwalk n = seq { if n > 0 then 
                         yield! rwalk (n-1)
                         yield n 
                      }

我发现第一个非常快,而第二个非常慢。 如果n = 10000,我的机器上生成此序列需要3秒,因此是二次时间。

二次时间是合理的,例如

seq { yield! {1; 2; ...; n-1}; yield n } seq { yield! {1; 2; ...; n-1}; yield n }转换为

Seq.append {1; 2; ...; n-1} {n}

我想这个追加操作应该是线性时间。 在第一个代码中,append操作是这样的: seq { yield n; yield! {n-1; n-2; ...; 1} } seq { yield n; yield! {n-1; n-2; ...; 1} } seq { yield n; yield! {n-1; n-2; ...; 1} } ,这需要花费不变的时间。

代码中的注释表示它是linear (也许这个线性不是线性时间)。 也许这种linear与使用序列的定制实现而不是Moand / F#计算表达式相关(如F#规范中所述,但是规范没有提到这样做的原因......)。

谁能澄清这里的模糊性? 非常感谢!

(ps因为这是一个语言设计和优化问题,我还附上了Haskell标签,看看那里的人是否有见解。)

yield! 出现在非尾部调用位置 ,它基本上与以下内容相同:

for v in <expr> do yield v

这个问题(以及为什么是二次方的原因)是对于递归调用,这会创建一个带有嵌套for循环的迭代器链。 您需要迭代<expr>为每个元素生成的整个序列,因此如果迭代是线性的,则得到二次时间(因为线性迭代发生在每个元素上)。

假设rwalk函数生成[ 9; 2; 3; 7 ] [ 9; 2; 3; 7 ] [ 9; 2; 3; 7 ] 在第一次迭代中,递归生成的序列有4个元素,因此你将迭代4个元素并添加1.在递归调用中,你将迭代3个元素并添加1等。使用图表,你可以看看那是二次的:

x
x x 
x x x
x x x x

此外,每个递归调用都会创建一个新的对象实例( IEnumerator ),因此也会有一些内存成本(尽管只是线性的)。

尾部调用位置 ,F#compiler / librar进行优化。 它将当前IEnumerable “替换”为递归调用返回的IEnumerable ,因此它不需要迭代它以生成所有元素 - 它只是返回(这也消除了内存开销)。

有关。 在C#lanaugage设计中已经讨论了同样的问题,并且有一篇关于它有趣论文 (它们的yield!名称是yield foreach )。

我不确定你在寻找什么样的答案。 您已经注意到,注释与编译器的行为不匹配。 我不能说这是一个与实现不同步的评论实例,还是它实际上是一个性能错误(例如,规范似乎没有提出任何具体的性能要求)。

但是,从理论上讲,编译器的机器应该可以生成一个在线性时间内对您的示例进行操作的实现。 实际上,甚至可以使用计算表达式在库中构建这样的实现。 这是一个粗略的例子,主要基于Tomas引用的论文:

open System.Collections
open System.Collections.Generic

type 'a nestedState = 
/// Nothing to yield
| Done 
/// Yield a single value before proceeding
| Val of 'a
/// Yield the results from a nested iterator before proceeding
| Enum of (unit -> 'a nestedState)
/// Yield just the results from a nested iterator
| Tail of (unit -> 'a nestedState)

type nestedSeq<'a>(ntor) =
  let getEnumerator() : IEnumerator<'a> =
    let stack = ref [ntor]
    let curr = ref Unchecked.defaultof<'a>
    let rec moveNext() =
      match !stack with
      | [] -> false
      | e::es as l -> 
          match e() with
          | Done -> stack := es; moveNext()  
          | Val(a) -> curr := a; true
          | Enum(e) -> stack := e :: l; moveNext()
          | Tail(e) -> stack := e :: es; moveNext()
    { new IEnumerator<'a> with
        member x.Current = !curr
      interface System.IDisposable with
        member x.Dispose() = () 
      interface IEnumerator with
        member x.MoveNext() = moveNext()
        member x.Current = box !curr
        member x.Reset() = failwith "Reset not supported" }
  member x.NestedEnumerator = ntor
  interface IEnumerable<'a> with
    member x.GetEnumerator() = getEnumerator()
  interface IEnumerable with
    member x.GetEnumerator() = upcast getEnumerator()

let getNestedEnumerator : 'a seq -> _ = function
| :? ('a nestedSeq) as n -> n.NestedEnumerator
| s -> 
    let e = s.GetEnumerator()
    fun () ->
      if e.MoveNext() then
        Val e.Current
      else
        Done

let states (arr : Lazy<_[]>) = 
  let state = ref -1 
  nestedSeq (fun () -> incr state; arr.Value.[!state]) :> seq<_>

type SeqBuilder() = 
  member s.Yield(x) =  
    states (lazy [| Val x; Done |])
  member s.Combine(x:'a seq, y:'a seq) = 
    states (lazy [| Enum (getNestedEnumerator x); Tail (getNestedEnumerator y) |])
  member s.Zero() =  
    states (lazy [| Done |])
  member s.Delay(f) = 
    states (lazy [| Tail (f() |> getNestedEnumerator) |])
  member s.YieldFrom(x) = x 
  member s.Bind(x:'a seq, f) = 
    let e = x.GetEnumerator() 
    nestedSeq (fun () -> 
                 if e.MoveNext() then  
                   Enum (f e.Current |> getNestedEnumerator) 
                 else  
                   Done) :> seq<_>

let seq = SeqBuilder()

let rec walkr n = seq { 
  if n > 0 then
    return! walkr (n-1)
    return n
}

let rec walkl n = seq {
  if n > 0 then
    return n
    return! walkl (n-1)
}

let time = 
  let watch = System.Diagnostics.Stopwatch.StartNew()
  walkr 10000 |> Seq.iter ignore
  watch.Stop()
  watch.Elapsed

请注意,我的SeqBuilder不健壮; 它缺少几个工作流成员,并且它没有做任何有关对象处理或错误处理的事情。 但是,它确实证明了SequenceBuilder 不需要在像你这样的例子上展示二次运行时间。

另请注意,这里存在时间空间权衡 - walkr n的嵌套迭代器将在O(n)时间内遍历序列,但它需要O(n)空间才能执行此操作。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM