【问题标题】:Compiling Tail-Call Optimization In Mutual Recursion Across C and Haskell在 C 和 Haskell 的相互递归中编译尾调用优化
【发布时间】:2015-11-03 18:19:23
【问题描述】:

我正在尝试使用 Haskell 中的外部函数接口。我想实现一个简单的测试,看看我是否可以进行相互递归。因此,我创建了以下 Haskell 代码:

module MutualRecursion where
import Data.Int

foreign import ccall countdownC::Int32->IO ()
foreign export ccall countdownHaskell::Int32->IO()

countdownHaskell::Int32->IO()
countdownHaskell n = print n >> if n > 0 then countdownC (pred n) else return ()

请注意,递归情况是对 countdownC 的调用,所以这应该是尾递归的。

在我的 C 代码中,我有

#include <stdio.h>

#include "MutualRecursionHaskell_stub.h"

void countdownC(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownHaskell(count-1);
}

int main(int argc, char* argv[])
{
    hs_init(&argc, &argv);

    countdownHaskell(10000);

    hs_exit();
    return 0;
}

这也是尾递归。那我做一个

MutualRecursion: MutualRecursionHaskell_stub
    ghc -O2 -no-hs-main MutualRecursionC.c MutualRecursionHaskell.o -o MutualRecursion
MutualRecursionHaskell_stub:
    ghc -O2 -c MutualRecursionHaskell.hs

并使用make MutualRecursion 编译。

而且...在运行时,它会在打印8991 后出现段错误。 作为一个确保 gcc 本身可以在相互递归中处理 tco 的测试,我做了

void countdownC2(int);

void countdownC(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownC2(count-1);
}

void countdownC2(int count)
{
    printf("%d\n", count);
    if(count > 0)
        return countdownC(count-1);
}

而且效果很好。它也适用于仅在 C 中和仅在 Haskell 中的单递归情况。

所以我的问题是,有没有办法向 GHC 表明对外部 C 函数的调用是尾递归的?我假设堆栈帧确实来自从 Haskell 到 C 的调用,而不是相反,因为 C 代码很明显是函数调用的返回。

【问题讨论】:

    标签: c haskell tail-call-optimization multiple-languages mutual-recursion


    【解决方案1】:

    我相信跨语言 C-Haskell 尾调用非常非常难以实现。

    我不知道确切的细节,但 C 运行时和 Haskell 运行时有很大的不同。据我所知,造成这种差异的主要因素是:

    • 不同的范例:纯函数式 vs 命令式
    • 垃圾收集与手动内存管理
    • 惰性语义 vs 严格语义

    考虑到这种差异,跨语言边界的优化类型几乎为零。或许,理论上,我们可以发明一个 ad hoc C 运行时和一个 Haskell 运行时,这样一些优化是可行的,但是 GHC 和 GCC 不是以这种方式设计的。

    只是为了展示潜在差异的示例,假设我们有以下 Haskell 代码

    p :: Int -> Bool
    p x = x==42
    
    main = if p 42
           then putStrLn "A"     -- A
           else putStrLn "B"     -- B
    

    main 的可能实现如下:

    • A的地址入栈
    • B的地址入栈
    • 42 推入堆栈
    • 跳转到p
    • A:打印“A”,跳转到结尾
    • B:打印“B”,跳转到结尾

    p的实现方式如下:

    • p: 从堆栈中弹出x
    • 从堆栈中弹出b
    • 从堆栈中弹出a
    • 针对 42 测试 x
    • 如果相等,跳转到a
    • 跳转到b

    注意p 是如何使用两个 返回地址调用的,每个返回地址对应一个可能的结果。这与 C 不同,后者的标准实现只使用一个返回地址。跨越边界时,编译器必须考虑这种差异并进行补偿。

    为了简单起见,上面我也没有考虑p 的参数是 thunk 的情况。 GHC 分配器也可以触发垃圾回收。

    请注意,上述虚构的实现实际上是 GHC 过去使用的(所谓的“推送/进入”STG 机器)。即使现在不再使用它,“eval/apply”STG 机器也只是稍微接近 C 运行时。我什至不确定 GHC 是否使用常规 C 堆栈:我认为它没有,使用它自己的堆栈。

    您可以查看GHC developer wiki 以查看详细信息。

    【讨论】:

    • 有没有办法防止重复返回位置?例如,我使用模式匹配编写了一个备用例程(基本情况为 0),但这没有帮助。一般来说,有没有办法 tel 告诉 GHC 以允许跨边界尾递归的方式进行编译?
    • @Crazycolorz5 调整运行时是一项非常艰巨的任务。您似乎认为 GHC 应该使其运行时适应 C 标准。要弄清楚这有多难,请考虑另一种方式:修改 GCC 以允许多次返回、垃圾收集等。这几乎是不可能的。目前的 GHC 甚至在遥远的未来都不太可能实现您的要求。
    【解决方案2】:

    虽然我不是 Haskel-C 互操作方面的专家,但我不认为从 C 到 Haskel 的调用可以是直接的函数调用 - 它很可能必须通过中介来设置环境。因此,您对 haskel 的调用实际上将包括对这个中介的调用。这个调用可能是由 gcc 优化的。但是从中介到实际 Haskel 例程的调用没有必要优化 - 所以我假设,这就是你正在处理的。您可以检查程序集输出以确保。

    【讨论】:

      猜你喜欢
      • 2021-01-02
      • 2015-06-16
      • 2012-10-14
      • 1970-01-01
      • 2021-05-30
      • 1970-01-01
      • 2014-07-24
      • 2018-06-05
      相关资源
      最近更新 更多