【问题标题】:Stack overflow in OCaml and F# but not in HaskellOCaml 和 F# 中的堆栈溢出,但在 Haskell 中没有
【发布时间】:2010-02-20 14:31:12
【问题描述】:

我一直在比较有趣的不同语言在执行以下程序时的速度: 对于 i 从 1 到 1000000 求和乘积 i*(sqrt i)

我的一个实现(不是唯一一个)是构造一个列表 [1..1000000],然后使用特定函数折叠。

该程序在 Haskell 中运行良好且快速(即使使用 foldl 而不是 foldl'),但在 OCaml 和 F# 中堆栈溢出。

这是 Haskell 代码:

test = foldl (\ a b -> a + b * (sqrt b)) 0

create 0 = []
create n = n:(create (n-1))

main = print (test (create 1000000))

这是 OCaml 的:

let test = List.fold_left (fun a b -> a +. (float_of_int b) *. (sqrt (float_of_int b))) 0. ;;

let rec create = function
    | 0 -> []
    | n -> n::(create (n-1)) ;;

print_float (test (create 1000000));;

为什么 OCaml/F# 实现堆栈溢出?

【问题讨论】:

    标签: haskell f# ocaml stack-overflow lazy-evaluation


    【解决方案1】:

    create 的 Haskell 代码懒惰地评估列表,即 foldl 需要元素。不需要一次性全部列出。

    相比之下,F# 和 OCaml 严格评估 create 列表,即代码尝试在将其传递给 fold_left 之前一次性生成整个列表。

    F# 中的一种可能性是在create 函数中使用seq 表达式:这会以与Haskell 相同的方式延迟生成列表。 (我不确定 OCaml 是否具有等效功能。)

    【讨论】:

    • OCaml 有 lazyLazy.force。它曾经是显而易见的 ML 端实现,任何人都可以通过引用闭包来编写它。 3.0 的运行时集成了特殊支持?以及 3.11.0 中的更多支持(模式匹配)。如今,GC 在强制暂停后的一段时间内删除了间接。 ralyx.inria.fr/2008/Raweb/gallium/uid48.html
    【解决方案2】:

    首先,如果您要对数字内容进行性能比较,列表不是最佳选择。尝试使用类似 vector 的包来实现快速数组。

    请注意,由于循环融合,您可以在 Haskell 中做得更好。通过将创建函数编写为枚举,编译器可以将创建步骤和折叠循环组合成一个不分配中间数据结构的循环。像这样进行一般融合的能力是 GHC Haskell 独有的。

    我将使用向量库(基于流的循环融合):

    import qualified Data.Vector as V
    
    test = V.foldl (\ a b -> a + b * sqrt b) 0
    
    create n = (V.enumFromTo 1 n)
    
    main = print (test (create 1000000))
    

    现在,在您的代码之前,编译器无法删除所有列表,我们最终会得到一个内部循环,例如:

    $wlgo :: Double# -> [Double] -> Double#
    
    $wlgo =
      \ (ww_sww :: Double#) (w_swy :: [Double]) ->
        case w_swy of _ {
          [] -> ww_sww;
          : x_aoY xs_aoZ ->
            case x_aoY of _ { D# x1_aql ->
            $wlgo
              (+##
                 ww_sww (*## x1_aql (sqrtDouble# x1_aql)))
              xs_aoZ
            }
        }
    
    $wcreate :: Double# -> [Double]
    
    $wcreate =
      \ (ww_swp :: Double#) ->
        case ==## ww_swp 0.0 of _ {
          False ->
            :
              @ Double
              (D# ww_swp)
              ($wcreate (-## ww_swp 1.0));
          True -> [] @ Double
        }
    

    注意有两个循环:创建生成(惰性)列表,折叠使用它。由于懒惰,该列表的成本很便宜,因此它运行得相当可观:

    $ time ./C
    4.000004999999896e14
    ./C  0.06s user 0.00s system 98% cpu 0.058 total
    

    然而,在融合下,我们只得到一个循环!

    main_$s$wfoldlM_loop :: Double# -> Double# -> Double#
    
    main_$s$wfoldlM_loop =
      \ (sc_sYc :: Double#) (sc1_sYd :: Double#) ->
        case <=## sc_sYc 1000000.5 of _ {
          False -> sc1_sYd;
          True ->
            main_$s$wfoldlM_loop
              (+## sc_sYc 1.0)
              (+##
                 sc1_sYd (*## sc_sYc (sqrtDouble# sc_sYc)))
    

    GHC 将我们的创建和测试步骤减少到一个没有使用列表的循环中。寄存器中只有 2 个双打。 循环次数减半,它的运行速度几乎是原来的两倍:

    $ ghc D.hs -Odph -fvia-C -optc-O3 -optc-march=native -fexcess-precision --make
    $ time ./D
    4.000005000001039e14
    ./D  0.04s user 0.00s system 95% cpu 0.038 total
    

    这是纯度保证提供的强大功能的一个很好的例子——编译器可以非常积极地重新排列你的代码。

    【讨论】:

    • 很奇怪,我的版本需要 44 毫秒才能执行,而你的版本需要 76 毫秒!
    • 它需要一个可熔双精度向量库的补丁(从向量的 darcs 版本中获取,http://code.haskell.org/vector)
    • @Farnand Pajot:你使用 dons 的命令行编译代码吗?我认为循环融合只有在启用优化时才会发生。
    • 是的,我做到了,我还将这些优化用于一个简单的尾递归函数(不涉及列表),执行时间缩短了 3 倍!
    【解决方案3】:

    修复代码使其在 F# 中工作的另一种方法是使用序列表达式,这些表达式也是延迟生成的,并且不会导致 StackOverflow(这在 OCaml 中不起作用,因为序列表达式是F# 特定)。代码如下:

    let test nums = Seq.fold (fun a b -> 
      a + (float b) * (sqrt (float b))) 0.0 nums
    
    let rec create count = seq {
      match count with
      | 0 -> do ()
      | n -> yield n
             yield! create (n-1) }
    
    printf "%f" (test (create 1000000));; 
    

    我不确定性能,但是编译器肯定会在 create 函数中进行优化,因此对生成的序列的迭代应该相当快。然而,序列表达式的目标主要是可读性(因为 F# 不是纯的,如果你真的需要它们,它为你提供了很多手动优化的空间,而不是需要优化你编写的纯代码的 Haskell)。无论如何,如果你有一些测试,你可以试一试!

    【讨论】:

    【解决方案4】:

    在这种形式中,您的 create 函数不是尾递归的。您可以将其重写为不会导致堆栈溢出的尾递归形式:

    let rec create n l =
        match n with 
        | 0 -> l
        | n -> create (n-1) (n::l)
    
    print_float (test (create 1000000 []));;
    

    【讨论】:

    • 确实如此,但由于某种原因它对性能有巨大影响(至少对 Haskell 而言),执行时间乘以 3。
    • @Fernand:在 Haskell 中,由于惰性求值,最好使用您的原始函数。在没有惰性求值的情况下,使用尾递归版本会更好——实际上几乎是必要的。
    • 在这个版本中,OCaml 不是在计算折叠之前仍然在内存中构建整个列表吗?在 Haskell 中,列表是在使用时构建的,因此它永远不会一次全部存储在内存中。
    • @mtnviewmark:在 OCaml 中,您可以使用作为 BatteriesIncluded 一部分的 LazyList 模块,以便它与 Haskell 版本非常相似:batteries.forge.ocamlcore.org/doc.preview%3Abatteries-beta1/…
    【解决方案5】:

    该程序在 Haskell 中运行良好且快速(即使使用 foldl 而不是 foldl'),但在 OCaml 和 F# 中堆栈溢出。

    您的 Haskell 使用惰性列表,但您的 OCaml/F# 使用严格列表,因此您的程序无与伦比。

    FWIW,在 F# 中使用按需序列的解决方案是:

    Seq.sumBy (fun i -> let i = float i in i * sqrt i) (seq {1..1000000})
    

    【讨论】:

      猜你喜欢
      • 2011-11-24
      • 1970-01-01
      • 2011-08-22
      • 2017-09-29
      • 2012-07-08
      • 1970-01-01
      • 2019-07-08
      • 2015-08-05
      • 1970-01-01
      相关资源
      最近更新 更多