【问题标题】:Verify that an OCaml function is tail-recursive验证 OCaml 函数是否是尾递归的
【发布时间】:2014-04-20 19:33:15
【问题描述】:

如何判断 OCaml 是否将特定函数识别为尾递归?特别想知道OCaml编译器是否识别Short-circuited operators and tail recursion


感谢杰弗里在下面的回答,我用简单的功能尝试了这个

let rec check_all l =
    match l with
    | [] -> true
    | hd :: tl ->
        hd && check_all tl

确实,它确实优化到:

camlTest__check_all_1008:
        .cfi_startproc
.L102:
        cmpl    $1, %eax
        je      .L100
        movl    (%eax), %ebx
        cmpl    $1, %ebx
        je      .L101
        movl    4(%eax), %eax
        jmp     .L102
        .align  16
.L101:
        movl    $1, %eax
        ret

【问题讨论】:

    标签: compiler-construction ocaml tail-recursion short-circuiting


    【解决方案1】:

    从 OCaml 4.03 开始​​,尽管Changes 文件中有错字,您可以在函数应用程序中使用@tailcall,如果不是这样,编译器会发出警告。

    (f [@tailcall]) x yf x y 不是尾调用时发出警告

    例子:

    $ cat tailrec.ml
    let rec f a x = if x <= 1 then a else (f [@tailcall]) (a * x) (x - 1)
    
    let rec g x = if x <= 1 then 1 else x * (g [@tailcall]) (x - 1)
    
    $ ocaml tailrec.ml
    
    File "tailrec.ml", line 3, characters 40-63:
    Warning 51: expected tailcall
    

    【讨论】:

      【解决方案2】:

      许多其他人比我更了解 OCaml 内部结构,但对于简单的函数,很容易在生成的 ocamlopt 汇编代码中看到尾递归:

      $ cat tailrec.ml
      let rec f a x = if x <= 1 then a else f (a * x) (x - 1)
      
      let rec g x = if x <= 1 then 1 else x * g (x - 1)
      $ ocamlopt -c -S tailrec.ml
      

      如果你忽略了很多额外的输出,你会看到f

      _camlTailrec__f_1008:
              .cfi_startproc
      .L101:
              cmpq    $3, %rbx
              jg      .L100
              ret
              .align  2
      .L100:
              movq    %rbx, %rdi
              addq    $-2, %rdi
              sarq    $1, %rbx
              decq    %rax
              imulq   %rbx, %rax
              incq    %rax
              movq    %rdi, %rbx
              jmp     .L101
              .cfi_endproc
      

      编译器已将递归调用改为循环(即函数为尾递归)。

      这是g 获得的内容:

              .cfi_startproc
              subq    $8, %rsp
              .cfi_adjust_cfa_offset  8
      .L103:
              cmpq    $3, %rax
              jg      .L102
              movq    $3, %rax
              addq    $8, %rsp
              .cfi_adjust_cfa_offset  -8
              ret
              .cfi_adjust_cfa_offset  8
              .align  2
      .L102:
              movq    %rax, 0(%rsp)
              addq    $-2, %rax
              call    _camlTailrec__g_1011
      .L104:
              movq    %rax, %rbx
              sarq    $1, %rbx
              movq    0(%rsp), %rax
              decq    %rax
              imulq   %rbx, %rax
              incq    %rax
              addq    $8, %rsp
              .cfi_adjust_cfa_offset  -8
              ret
              .cfi_adjust_cfa_offset  8
              .cfi_endproc
      

      递归由实际的递归调用(不是尾递归)处理。

      正如我所说,如果您比我更了解 OCaml 中间形式,可能会有更好的方法来解决这个问题。

      【讨论】:

      • 好答案 (+1)。准确地说,不应该谈论尾调用和尾调用优化吗?真正的问题是 OCaml 是否优化尾调用,而不是它是否识别它们(无论这意味着什么)。
      • 随着事情变得越来越复杂,这些信息也在-annot选项的输出中提供给编译器,一个显示类型注释的工具可以用尾调用信息装饰文件。 ocamlopt[.opt]-dlinear 选项也将在带有尾调用的修改后的源输出中具有注释。
      【解决方案3】:

      我想知道,为什么没有人告诉古老的 -annot 选项,它将为所有调用转储注释。虽然使用汇编是最可靠的方法,但并不是每个人都擅长阅读汇编。但是使用 annot 非常简单,您甚至可以将其自动化。例如,假设您的代码在test.ml 文件中,我们可以使用以下一行代码自动检查所有调用是否位于尾部位置:

       ocamlc -annot test.ml && if grep -A1 call test.annot | grep stack; then echo "non tailrecursive"; exit 1; fi
      

      ocaml -annot test.ml 将编译一个文件并创建一个 test.annot 文件,该文件将包含每个表达式的注释。 grep -A1 call test.annot 将提取所有调用注释,并查看它们的内容。如果至少有一个堆栈调用,grep stack 将返回 true。

      实际上甚至还有一个 emacs 补充,您可以在 the ocaml 存储库中找到它,它将从 annot 文件中提取此信息。例如,有一个caml-types-show-call 函数,它将显示在该点指定的一种函数调用。但是,此功能目前有一个错误(似乎不再支持),要修复它,您需要对其应用以下补丁:

      --- a/emacs/caml-types.el
      +++ b/emacs/caml-types.el
      @@ -221,7 +221,7 @@ See `caml-types-location-re' for annotation file format."
                     (right (caml-types-get-pos target-buf (elt node 1)))
                     (kind (cdr (assoc "call" (elt node 2)))))
                 (move-overlay caml-types-expr-ovl left right target-buf)
      -          (caml-types-feedback kind)))))
      +          (caml-types-feedback "call: %s" kind)))))
           (if (and (= arg 4)
                    (not (window-live-p (get-buffer-window caml-types-buffer))))
               (display-buffer caml-types-buffer))
      

      【讨论】:

      • 或许你应该提交一份 PR?
      • 可能,但我不想让 OCaml 开发人员负担如此小的 PR(有很多待处理的 PR),更好的办法是将这段代码提取到单独的 repo 中。另外,我相信,我在 ocaml/merlin 中也看到过类似的情况。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-10
      • 1970-01-01
      • 1970-01-01
      • 2012-10-17
      • 2021-11-25
      相关资源
      最近更新 更多