【问题标题】:Stack overflow despite tail call position but only in 64-bit尽管有尾调用位置但堆栈溢出但仅在 64 位
【发布时间】:2016-06-15 13:11:33
【问题描述】:

源自this question,我有这个小F#代码(github)根据正态分布生成随机值:

// val nextSingle : (unit -> float32)
let nextSingle =
    let r = System.Random()
    r.NextDouble >> float32

// val gauss : (float32 -> float32 -> seq<float32>)
let gauss mean stdDev =
    let rec gauss ready = seq {
        match ready with
        | Some spare ->
            yield spare * stdDev + mean
            yield! gauss None
        | _ ->
            let rec loop () =
                let u = nextSingle() * 2.f - 1.f
                let v = nextSingle() * 2.f - 1.f
                let s = pown u 2 + pown v 2
                if s >= 1.f || s = 0.f then loop() else
                u, v, s
            let u, v, s = loop()
            let mul = (*)(sqrt(-2.f * log s / s))
            yield mul u * stdDev + mean
            yield! mul v |> Some |> gauss
    }
    gauss None

在我看来,这似乎只应在尾调用位置调用自身,因此在启用 TCO 时永远不会导致StackOverflowException。但在运行 64 位 时它确实在运行 32 位(即项目设置中的“首选 32 位”复选框)时。

我正在使用 .NET Framework 4.5.2 和 F# 4.4.0.0。

谁能解释造成问题的原因?

【问题讨论】:

  • 您使用的是哪个 .NET 版本?
  • 我发现这些文章谈论的是尾调用优化using .NET 2.0 vs using .NET 4.0,也许他们可以提供帮助。
  • 另外,您使用的是什么版本的 F#?我使用 F#4,在调试时我根本看不到堆栈增长。我反汇编了 IL 代码,看起来 F#4 生成的代码不应该 stackoverflow。
  • 请附上完整的复制品。调用代码是什么样的?序列表达式中的“尾递归”可能会令人困惑,因为序列实际上并没有自行推进,其他一些代码会。

标签: f# 64-bit stack-overflow 32-bit tail-recursion


【解决方案1】:

看起来像是编译器的序列表达式编译机制中的一个错误。这是一个简化的重现:

let rec loop r = seq {
    if r > 0 then
        let rec unused() = unused()
        yield r
        yield! loop r
}

printfn "%i" (Seq.nth 10000000 (loop 1))

显然,未使用的递归定义的存在不应该影响这是否会产生堆栈溢出,但确实会。

【讨论】:

  • 我已在GitHub 上将此作为问题提交。
  • 是直觉还是非直觉推理导致了问题?我很想知道这是否只是使用IL 检查来解决基本问题,并结合使用 VS 社区版可以做到的最低限度,还是你使用了更多的东西?我正在努力提高解决 TCO 问题的能力。如果您希望将此作为单独的 SO 问题,我将很乐意这样做。另外,您从哪个版本的代码开始,GitHub 完整版、GitHub 最小版或问题中的示例,因为我想独立弄清楚。
  • @GuyCoder - 我的推理是这样的:在查看计算表达式时谈论“尾调用”可能会产生误导 - 确实没有这样的事情(例如,参见 stackoverflow.com/a/6615060/82959相关讨论,但与 sequence 表达式无关)。所以像gauss 这样的东西本身不会溢出堆栈,只有调用它的代码才能让它溢出。
  • @GuyCoder - 但是一旦我看到调用代码就像Seq.nth,这意味着几乎可以肯定存在编译器错误,因为“tail yield!ing”的模式在序列表达式应该得到优化(不确定这是否是规范的一部分,但我认为它是众所周知的)。因此,这只是查看初始复制的哪些部分是必要的情况。用非递归定义替换原始代码中的 loop 会使 repro 停止失败,删除模式匹配也是如此。
  • @GuyCoder - 我没有发现查看 IL 有帮助(在序列表达式的编译中涉及很多编译器生成的机器),我只是尝试在源代码级别和经验上最小化重现测试行为。
猜你喜欢
  • 2013-11-05
  • 2012-07-15
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 1970-01-01
  • 2015-12-01
  • 2015-10-16
  • 1970-01-01
相关资源
最近更新 更多